require 'spec_helper'

# A type and provider that can be purged
Puppet::Type.newtype(:purgeable_test) do
  ensurable
  newparam(:name) {}
end
Puppet::Type.type(:purgeable_test).provide(:purgeable_test) do
  def self.instances
    []
  end
end

resources = Puppet::Type.type(:resources)

# There are still plenty of tests to port over from test/.
describe resources do
  before :each do
    described_class.reset_system_users_max_uid!
  end

  context "when initializing" do
    it "should fail if the specified resource type does not exist" do
      allow(Puppet::Type).to receive(:type) do
        expect(x.to_s.downcase).to eq("resources")
      end.and_return(resources)
      expect(Puppet::Type).to receive(:type).with("nosuchtype").and_return(nil)
      expect { resources.new :name => "nosuchtype" }.to raise_error(Puppet::Error)
    end

    it "should not fail when the specified resource type exists" do
      expect { resources.new :name => "file" }.not_to raise_error
    end

    it "should set its :resource_type attribute" do
      expect(resources.new(:name => "file").resource_type).to eq(Puppet::Type.type(:file))
    end
  end

  context "purge" do
    let (:instance) { described_class.new(:name => 'file') }

    it "defaults to false" do
      expect(instance[:purge]).to be_falsey
    end

    it "can be set to false" do
      instance[:purge] = 'false'
    end

    it "cannot be set to true for a resource type that does not accept ensure" do
      allow(instance.resource_type).to receive(:validproperty?).with(:ensure).and_return(false)
      expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error, /Purging is only supported on types that accept 'ensure'/
    end

    it "cannot be set to true for a resource type that does not have instances" do
      allow(instance.resource_type).to receive(:respond_to?).with(:instances).and_return(false)
      expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error, /Purging resources of type file is not supported/
    end

    it "can be set to true for a resource type that has instances and can accept ensure" do
      allow(instance.resource_type).to receive(:validproperty?).and_return(true)
      expect { instance[:purge] = 'yes' }.to_not raise_error
    end
  end

  context "#check_user purge behaviour" do
    context "with unless_system_user => true" do
      before do
        @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true
        @res.catalog = Puppet::Resource::Catalog.new
        allow(Puppet::FileSystem).to receive(:exist?).with('/etc/login.defs').and_return(false)
      end

      it "should never purge hardcoded system users" do
        %w{root nobody bin noaccess daemon sys}.each do |sys_user|
          expect(@res.user_check(Puppet::Type.type(:user).new(:name => sys_user))).to be_falsey
        end
      end

      it "should not purge system users if unless_system_user => true" do
        user_hash = {:name => 'system_user', :uid => 125, :system => true}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(@res.user_check(user)).to be_falsey
      end

      it "should purge non-system users if unless_system_user => true" do
        user_hash = {:name => 'system_user', :uid => described_class.system_users_max_uid + 1, :system => true}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(@res.user_check(user)).to be_truthy
      end

      it "should not purge system users under 600 if unless_system_user => 600" do
        res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => 600
        res.catalog = Puppet::Resource::Catalog.new
        user_hash = {:name => 'system_user', :uid => 500, :system => true}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(res.user_check(user)).to be_falsey
      end

      it "should not purge Windows system users" do
        res = Puppet::Type.type(:resources).new :name => :user, :purge => true
        res.catalog = Puppet::Resource::Catalog.new
        user_hash = {:name => 'Administrator', :uid => 'S-1-5-21-12345-500'}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(res.user_check(user)).to be_falsey
      end

      it "should not purge Windows system users" do
        res = Puppet::Type.type(:resources).new :name => :user, :purge => true
        res.catalog = Puppet::Resource::Catalog.new
        user_hash = {:name => 'other', :uid => 'S-1-5-21-12345-1001'}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(res.user_check(user)).to be_truthy
      end
    end

    %w(FreeBSD OpenBSD).each do |os|
      context "on #{os}" do
        before :each do
          allow(Facter).to receive(:value).with(:kernel).and_return(os)
          allow(Facter).to receive(:value).with(:operatingsystem).and_return(os)
          allow(Facter).to receive(:value).with(:osfamily).and_return(os)
          allow(Puppet::FileSystem).to receive(:exist?).with('/etc/login.defs').and_return(false)
          @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true
          @res.catalog = Puppet::Resource::Catalog.new
        end

        it "should not purge system users under 1000" do
          user_hash = {:name => 'system_user', :uid => 999}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end

        it "should purge users over 999" do
          user_hash = {:name => 'system_user', :uid => 1000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_truthy
        end
      end
    end

    context 'with login.defs present' do
      before :each do
        expect(Puppet::FileSystem).to receive(:exist?).with('/etc/login.defs').and_return(true)
        expect(Puppet::FileSystem).to receive(:each_line).with('/etc/login.defs').and_yield(' UID_MIN         1234 # UID_MIN comment ')
        @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true
        @res.catalog = Puppet::Resource::Catalog.new
      end

      it 'should not purge a system user' do
        user_hash = {:name => 'system_user', :uid => 1233}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(@res.user_check(user)).to be_falsey
      end

      it 'should purge a non-system user' do
        user_hash = {:name => 'system_user', :uid => 1234}
        user = Puppet::Type.type(:user).new(user_hash)
        allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
        expect(@res.user_check(user)).to be_truthy
      end
    end

    context "with unless_uid" do
      context "with a uid array" do
        before do
          @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => [15_000, 15_001, 15_002]
          @res.catalog = Puppet::Resource::Catalog.new
        end

        it "should purge uids that are not in a specified array" do
          user_hash = {:name => 'special_user', :uid => 25_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_truthy
        end

        it "should not purge uids that are in a specified array" do
          user_hash = {:name => 'special_user', :uid => 15000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end
      end

      context "with a single integer uid" do
        before do
          @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => 15_000
          @res.catalog = Puppet::Resource::Catalog.new
        end

        it "should purge uids that are not specified" do
          user_hash = {:name => 'special_user', :uid => 25_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_truthy
        end

        it "should not purge uids that are specified" do
          user_hash = {:name => 'special_user', :uid => 15_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end
      end

      context "with a single string uid" do
        before do
          @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => '15000'
          @res.catalog = Puppet::Resource::Catalog.new
        end

        it "should purge uids that are not specified" do
          user_hash = {:name => 'special_user', :uid => 25_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_truthy
        end

        it "should not purge uids that are specified" do
          user_hash = {:name => 'special_user', :uid => 15_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end
      end

      context "with a mixed uid array" do
        before do
          @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => ['15000', 16_666]
          @res.catalog = Puppet::Resource::Catalog.new
        end

        it "should not purge ids in the range" do
          user_hash = {:name => 'special_user', :uid => 15_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end

        it "should not purge specified ids" do
          user_hash = {:name => 'special_user', :uid => 16_666}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_falsey
        end

        it "should purge unspecified ids" do
          user_hash = {:name => 'special_user', :uid => 17_000}
          user = Puppet::Type.type(:user).new(user_hash)
          allow(user).to receive(:retrieve_resource).and_return(Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash))
          expect(@res.user_check(user)).to be_truthy
        end
      end
    end
  end

  context "#generate" do
    before do
      @purgee = Puppet::Type.type(:purgeable_test).new(:name => 'localhost')
      @catalog = Puppet::Resource::Catalog.new
    end

    context "when the catalog contains a purging resource with an alias" do
      before do
        @resource = Puppet::Type.type(:resources).new(:name => "purgeable_test", :purge => true)
        @catalog.add_resource @resource
        @catalog.alias(@resource, "purgeable_test_alias")
      end

      it "should not copy the alias metaparameter" do
        allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@purgee])
        generated = @resource.generate.first
        expect(generated[:alias]).to be_nil
      end
    end

    context "when dealing with non-purging resources" do
      before do
        @resources = Puppet::Type.type(:resources).new(:name => 'purgeable_test')
      end

      it "should not generate any resource" do
        expect(@resources.generate).to be_empty
      end
    end

    context "when the catalog contains a purging resource" do
      before do
        @resources = Puppet::Type.type(:resources).new(:name => 'purgeable_test', :purge => true)
        @purgeable_resource = Puppet::Type.type(:purgeable_test).new(:name => 'localhost')
        @catalog.add_resource @resources
      end

      it "should not generate a duplicate of that resource" do
        allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@purgee])
        @catalog.add_resource @purgee
        expect(@resources.generate.collect { |r| r.ref }).not_to include(@purgee.ref)
      end

      it "should not include the skipped system users" do
        res = Puppet::Type.type(:resources).new :name => :user, :purge => true
        res.catalog = Puppet::Resource::Catalog.new

        root = Puppet::Type.type(:user).new(:name => "root")
        expect(Puppet::Type.type(:user)).to receive(:instances).and_return([root])

        list = res.generate

        names = list.collect { |r| r[:name] }
        expect(names).not_to be_include("root")
      end

      context "when generating a purgeable resource" do
        it "should be included in the generated resources" do
          allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@purgeable_resource])
          expect(@resources.generate.collect { |r| r.ref }).to include(@purgeable_resource.ref)
        end

        context "when the instance's do not have an ensure property" do
          it "should not be included in the generated resources" do
            @no_ensure_resource = Puppet::Type.type(:exec).new(:name => "#{File.expand_path('/usr/bin/env')} echo")
            allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@no_ensure_resource])
            expect(@resources.generate.collect { |r| r.ref }).not_to include(@no_ensure_resource.ref)
          end
        end

        context "when the instance's ensure property does not accept absent" do
          it "should not be included in the generated resources" do
            # We have a :confine block that calls execute in our upstart provider, which fails
            # on jruby. Thus, we stub it out here since we don't care to do any assertions on it.
            # This is only an issue if you're running these unit tests on a platform where upstart
            # is a default provider, like Ubuntu trusty.
            allow(Puppet::Util::Execution).to receive(:execute)

            @no_absent_resource = Puppet::Type.type(:service).new(:name => 'foobar')
            allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@no_absent_resource])
            expect(@resources.generate.collect { |r| r.ref }).not_to include(@no_absent_resource.ref)
          end
        end

        context "when checking the instance fails" do
          it "should not be included in the generated resources" do
            @purgeable_resource = Puppet::Type.type(:purgeable_test).new(:name => 'foobar')
            allow(Puppet::Type.type(:purgeable_test)).to receive(:instances).and_return([@purgeable_resource])
            expect(@resources).to receive(:check).with(@purgeable_resource).and_return(false)
            expect(@resources.generate.collect { |r| r.ref }).not_to include(@purgeable_resource.ref)
          end
        end
      end
    end
  end
end
