require 'spec_helper'

describe Puppet::Type.type(:package).provider(:dnfmodule) do
  include PuppetSpec::Fixtures

  let(:dnf_version) do
    <<-DNF_OUTPUT
    4.0.9
      Installed: dnf-0:4.0.9.2-5.el8.noarch at Wed 29 May 2019 07:05:05 AM GMT
      Built    : Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla> at Thu 14 Feb 2019 12:04:07 PM GMT

      Installed: rpm-0:4.14.2-9.el8.x86_64 at Wed 29 May 2019 07:04:33 AM GMT
      Built    : Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla> at Thu 20 Dec 2018 01:30:03 PM GMT
    DNF_OUTPUT
  end

  let(:execute_options) do
    {:failonfail => true, :combine => true, :custom_environment => {}}
  end

  let(:packages) { File.read(my_fixture("dnf-module-list.txt")) }
  let(:dnf_path) { '/usr/bin/dnf' }

  before(:each) { allow(Puppet::Util).to receive(:which).with('/usr/bin/dnf').and_return(dnf_path) }

  it "should have lower specificity" do
    allow(Facter).to receive(:value).with('os.family').and_return(:redhat)
    allow(Facter).to receive(:value).with('os.name').and_return(:redhat)
    allow(Facter).to receive(:value).with('os.release.major').and_return('8')
    expect(described_class.specificity).to be < 200
  end

  describe "should be an opt-in provider" do
    Array(4..8).each do |ver|
      it "should not be default for redhat #{ver}" do
        allow(Facter).to receive(:value).with('os.name').and_return('redhat')
        allow(Facter).to receive(:value).with('os.family').and_return('redhat')
        allow(Facter).to receive(:value).with('os.release.major').and_return(ver.to_s)
        expect(described_class).not_to be_default
      end
    end
  end

  describe "handling dnf versions" do
    before(:each) do
      expect(Puppet::Type::Package::ProviderDnfmodule).to receive(:execute)
          .with(["/usr/bin/dnf", "--version"])
          .and_return(Puppet::Util::Execution::ProcessOutput.new(dnf_version, 0)).at_most(:once)
      expect(Puppet::Util::Execution).to receive(:execute)
          .with(["/usr/bin/dnf", "--version"], execute_options)
          .and_return(Puppet::Util::Execution::ProcessOutput.new(dnf_version, 0))
    end

    before(:each) { described_class.instance_variable_set("@current_version", nil) }

    describe "with a supported dnf version" do
      it "correctly parses the version" do
        expect(described_class.current_version).to eq('4.0.9')
      end
    end

    describe "with an unsupported dnf version" do
      let(:dnf_version) do
        <<-DNF_OUTPUT
        2.7.5
          Installed: dnf-0:2.7.5-12.fc28.noarch at Mon 13 Aug 2018 11:05:27 PM GMT
          Built    : Fedora Project at Wed 18 Apr 2018 02:29:51 PM GMT

          Installed: rpm-0:4.14.1-7.fc28.x86_64 at Mon 13 Aug 2018 11:05:25 PM GMT
          Built    : Fedora Project at Mon 19 Feb 2018 09:29:01 AM GMT
        DNF_OUTPUT
      end

      it "correctly parses the version" do
        expect(described_class.current_version).to eq('2.7.5')
      end

      it "raises an error when attempting prefetch" do
        expect { described_class.prefetch('anything') }.to raise_error(Puppet::Error, "Modules are not supported on DNF versions lower than 3.0.1")
      end
    end
  end

  describe "when ensuring a module" do
    let(:name) { 'baz' }

    let(:resource) do
      Puppet::Type.type(:package).new(
        :name => name,
        :provider => 'dnfmodule',
      )
    end

    let(:provider) do
      provider = described_class.new
      provider.resource = resource
      provider
    end

    describe 'provider features' do
      it { is_expected.to be_versionable }
      it { is_expected.to be_installable }
      it { is_expected.to be_uninstallable }
    end

    context "when installing a new module" do
      before do
        provider.instance_variable_get('@property_hash')[:ensure] = :absent
      end

      it "should not reset the module stream when package is absent" do
        resource[:ensure] = :present
        expect(provider).not_to receive(:uninstall)
        expect(provider).to receive(:execute)
        provider.install
      end

      it "should not reset the module stream when package is purged" do
        provider.instance_variable_get('@property_hash')[:ensure] = :purged
        resource[:ensure] = :present
        expect(provider).not_to receive(:uninstall)
        expect(provider).to receive(:execute)
        provider.install
      end

      it "should just enable the module if it has no default profile (missing groups or modules)" do
        dnf_exception = Puppet::ExecutionFailure.new("Error: Problems in request:\nmissing groups or modules: #{resource[:name]}")
        allow(provider).to receive(:execute).with(array_including('install')).and_raise(dnf_exception)
        resource[:ensure] = :present
        expect(provider).to receive(:execute).with(array_including('install')).ordered
        expect(provider).to receive(:execute).with(array_including('enable')).ordered
        provider.install
      end

      it "should just enable the module with the right stream if it has no default profile (missing groups or modules)" do
        stream = '12.3'
        dnf_exception = Puppet::ExecutionFailure.new("Error: Problems in request:\nmissing groups or modules: #{resource[:name]}:#{stream}")
        allow(provider).to receive(:execute).with(array_including('install')).and_raise(dnf_exception)
        resource[:ensure] = stream
        expect(provider).to receive(:execute).with(array_including('install')).ordered
        expect(provider).to receive(:execute).with(array_including('enable')).ordered
        provider.install
      end

      it "should just enable the module if it has no default profile (broken groups or modules)" do
        dnf_exception = Puppet::ExecutionFailure.new("Error: Problems in request:\nbroken groups or modules: #{resource[:name]}")
        allow(provider).to receive(:execute).with(array_including('install')).and_raise(dnf_exception)
        resource[:ensure] = :present
        expect(provider).to receive(:execute).with(array_including('install')).ordered
        expect(provider).to receive(:execute).with(array_including('enable')).ordered
        provider.install
      end

      it "should just enable the module with the right stream if it has no default profile (broken groups or modules)" do
        stream = '12.3'
        dnf_exception = Puppet::ExecutionFailure.new("Error: Problems in request:\nbroken groups or modules: #{resource[:name]}:#{stream}")
        allow(provider).to receive(:execute).with(array_including('install')).and_raise(dnf_exception)
        resource[:ensure] = stream
        expect(provider).to receive(:execute).with(array_including('install')).ordered
        expect(provider).to receive(:execute).with(array_including('enable')).ordered
        provider.install
      end

      it "should just enable the module if enable_only = true" do
        resource[:ensure] = :present
        resource[:enable_only] = true
        expect(provider).to receive(:execute).with(array_including('enable'))
        expect(provider).not_to receive(:execute).with(array_including('install'))
        provider.install
      end

      it "should install the default stream and flavor" do
        resource[:ensure] = :present
        expect(provider).to receive(:execute).with(array_including('baz'))
        provider.install
      end

      it "should install a specific stream" do
        resource[:ensure] = '9.6'
        expect(provider).to receive(:execute).with(array_including('baz:9.6'))
        provider.install
      end

      it "should install a specific flavor" do
        resource[:ensure] = :present
        resource[:flavor] = 'minimal'
        expect(provider).to receive(:execute).with(array_including('baz/minimal'))
        provider.install
      end

      it "should install a specific flavor and stream" do
        resource[:ensure] = '9.6'
        resource[:flavor] = 'minimal'
        expect(provider).to receive(:execute).with(array_including('baz:9.6/minimal'))
        provider.install
      end
    end

    context "when ensuring a specific version on top of another stream" do
      before do
        provider.instance_variable_get('@property_hash')[:ensure] = '9.6'
      end

      it "should remove existing packages and reset the module stream before installing" do
        resource[:ensure] = '10'
        expect(provider).to receive(:execute).thrice.with(array_including(/remove|reset|install/))
        provider.install
      end
    end

    context "with an installed flavor" do
      before do
        provider.instance_variable_get('@property_hash')[:flavor] = 'minimal'
      end

      it "should remove existing packages and reset the module stream before installing another flavor" do
        resource[:flavor] = 'common'
        expect(provider).to receive(:execute).thrice.with(array_including(/remove|reset|install/))
        provider.flavor = resource[:flavor]
      end

      it "should not do anything if the flavor doesn't change" do
        resource[:flavor] = 'minimal'
        expect(provider).not_to receive(:execute)
        provider.flavor = resource[:flavor]
      end

      it "should return the existing flavor" do
        expect(provider.flavor).to eq('minimal')
      end
    end

    context "when disabling a module" do

      it "executed the disable command" do
        resource[:ensure] = :disabled
        expect(provider).to receive(:execute).with(array_including('disable'))
        provider.disable
      end

      it "does not try to disable if package is already disabled" do
        allow(described_class).to receive(:command).with(:dnf).and_return(dnf_path)
        allow(Puppet::Util::Execution).to receive(:execute)
          .with("/usr/bin/dnf module list -y")
          .and_return("baz 1.2 [d][x] common [d], complete  Package Description")
        resource[:ensure] = :disabled
        expect(provider).to be_insync(:disabled)
      end
    end
  end

  context "parsing the output of module list" do
    before { allow(described_class).to receive(:command).with(:dnf).and_return(dnf_path) }

    it "returns an array of enabled modules" do
      allow(Puppet::Util::Execution).to receive(:execute)
        .with("/usr/bin/dnf module list -y")
        .and_return(packages)

      enabled_packages = described_class.instances.map { |package| package.properties }
      expected_packages = [{name: "389-ds", ensure: "1.4", flavor: :absent, provider: :dnfmodule},
                           {name: "gimp", ensure: "2.8", flavor: "devel", provider: :dnfmodule},
                           {name: "mariadb", ensure: "10.3", flavor: "client", provider: :dnfmodule},
                           {name: "nodejs", ensure: "10", flavor: "minimal", provider: :dnfmodule},
                           {name: "perl", ensure: "5.26", flavor: "minimal", provider: :dnfmodule},
                           {name: "postgresql", ensure: "10", flavor: "server", provider: :dnfmodule},
                           {name: "ruby", ensure: "2.5", flavor: :absent, provider: :dnfmodule},
                           {name: "rust-toolset", ensure: "rhel8", flavor: "common", provider: :dnfmodule},
                           {name: "subversion", ensure: "1.10", flavor: "server", provider: :dnfmodule},
                           {name: "swig", ensure: :disabled, flavor: :absent, provider: :dnfmodule},
                           {name: "virt", ensure: :disabled, flavor: :absent, provider: :dnfmodule}]

      expect(enabled_packages).to eql(expected_packages)
    end
  end
end
