require 'spec_helper'
require 'stringio'

describe Puppet::Type.type(:package).provider(:dpkg) do
  let(:bash_version) { '4.2-5ubuntu3' }
  let(:bash_installed_output) { "install ok installed bash #{bash_version}\n" }
  let(:bash_installed_io) { StringIO.new(bash_installed_output) }
  let(:vim_installed_output) { "install ok installed vim 2:7.3.547-6ubuntu5\n" }
  let(:all_installed_io) { StringIO.new([bash_installed_output, vim_installed_output].join) }
  let(:args) { ['-W', '--showformat', %Q{'${Status} ${Package} ${Version}\\n'}] }
  let(:execute_options) do
    {:failonfail => true, :combine => true, :custom_environment => {}}
  end
  let(:resource_name) { 'package' }
  let(:resource) { stub 'resource', :[] => resource_name }
  let(:provider) { described_class.new(resource) }

  it "has documentation" do
    expect(described_class.doc).to be_instance_of(String)
  end

  context "when listing all instances" do
    let(:execpipe_args) { args.unshift('myquery') }

    before do
      described_class.stubs(:command).with(:dpkgquery).returns 'myquery'
    end

    it "creates and return an instance for a single dpkg-query entry" do
      Puppet::Util::Execution.expects(:execpipe).with(execpipe_args).yields bash_installed_io

      installed = mock 'bash'
      described_class.expects(:new).with(:ensure => "4.2-5ubuntu3", :error => "ok", :desired => "install", :name => "bash", :status => "installed", :provider => :dpkg).returns installed

      expect(described_class.instances).to eq([installed])
    end

    it "parses multiple dpkg-query multi-line entries in the output" do
      Puppet::Util::Execution.expects(:execpipe).with(execpipe_args).yields all_installed_io

      bash = mock 'bash'
      described_class.expects(:new).with(:ensure => "4.2-5ubuntu3", :error => "ok", :desired => "install", :name => "bash", :status => "installed", :provider => :dpkg).returns bash
      vim = mock 'vim'
      described_class.expects(:new).with(:ensure => "2:7.3.547-6ubuntu5", :error => "ok", :desired => "install", :name => "vim", :status => "installed", :provider => :dpkg).returns vim

      expect(described_class.instances).to eq([bash, vim])
    end

    it "continues without failing if it encounters bad lines between good entries" do
      Puppet::Util::Execution.expects(:execpipe).with(execpipe_args).yields StringIO.new([bash_installed_output, "foobar\n", vim_installed_output].join)

      bash = mock 'bash'
      vim = mock 'vim'
      described_class.expects(:new).twice.returns(bash, vim)

      expect(described_class.instances).to eq([bash, vim])
    end
  end

  context "when querying the current state" do
    let(:dpkgquery_path) { '/bin/dpkg-query' }
    let(:query_args) do
      args.unshift(dpkgquery_path)
      args.push(resource_name)
    end

    def dpkg_query_execution_returns(output)
      Puppet::Util::Execution.expects(:execute).with(query_args, execute_options).returns(Puppet::Util::Execution::ProcessOutput.new(output, 0))
    end

    before do
      Puppet::Util.stubs(:which).with('/usr/bin/dpkg-query').returns(dpkgquery_path)
    end

    it "considers the package purged if dpkg-query fails" do
      Puppet::Util::Execution.expects(:execute).with(query_args, execute_options).raises Puppet::ExecutionFailure.new("eh")

      expect(provider.query[:ensure]).to eq(:purged)
    end

    it "returns a hash of the found package status for an installed package" do
      dpkg_query_execution_returns(bash_installed_output)

      expect(provider.query).to eq({:ensure => "4.2-5ubuntu3", :error => "ok", :desired => "install", :name => "bash", :status => "installed", :provider => :dpkg})
    end

    it "considers the package absent if the dpkg-query result cannot be interpreted" do
      dpkg_query_execution_returns('some-bad-data')

      expect(provider.query[:ensure]).to eq(:absent)
    end

    it "fails if an error is discovered" do
      dpkg_query_execution_returns(bash_installed_output.gsub("ok","error"))

      expect { provider.query }.to raise_error(Puppet::Error)
    end

    it "considers the package purged if it is marked 'not-installed'" do
      not_installed_bash = bash_installed_output.gsub("installed", "not-installed")
      not_installed_bash.gsub!(bash_version, "")
      dpkg_query_execution_returns(not_installed_bash)

      expect(provider.query[:ensure]).to eq(:purged)
    end

    it "considers the package absent if it is marked 'config-files'" do
      dpkg_query_execution_returns(bash_installed_output.gsub("installed","config-files"))
      expect(provider.query[:ensure]).to eq(:absent)
    end

    it "considers the package absent if it is marked 'half-installed'" do
      dpkg_query_execution_returns(bash_installed_output.gsub("installed","half-installed"))
      expect(provider.query[:ensure]).to eq(:absent)
    end

    it "considers the package absent if it is marked 'unpacked'" do
      dpkg_query_execution_returns(bash_installed_output.gsub("installed","unpacked"))
      expect(provider.query[:ensure]).to eq(:absent)
    end

    it "considers the package absent if it is marked 'half-configured'" do
      dpkg_query_execution_returns(bash_installed_output.gsub("installed","half-configured"))
      expect(provider.query[:ensure]).to eq(:absent)
    end

    it "considers the package held if its state is 'hold'" do
      dpkg_query_execution_returns(bash_installed_output.gsub("install","hold"))
      expect(provider.query[:ensure]).to eq(:held)
    end

    context "parsing tests" do
      let(:resource_name) { 'name' }
      let(:package_hash) do
        {
          :desired => 'desired',
          :error => 'ok',
          :status => 'status',
          :name => resource_name,
          :ensure => 'ensure',
          :provider => :dpkg,
        }
      end
      let(:package_not_found_hash) do
        {:ensure => :purged, :status => 'missing', :name => resource_name, :error => 'ok'}
      end

      def parser_test(dpkg_output_string, gold_hash, number_of_debug_logs = 0)
        dpkg_query_execution_returns(dpkg_output_string)
        Puppet.expects(:warning).never
        Puppet.expects(:debug).times(number_of_debug_logs)

        expect(provider.query).to eq(gold_hash)
      end

      it "parses properly even if optional ensure field is missing" do
        no_ensure = 'desired ok status name '
        parser_test(no_ensure, package_hash.merge(:ensure => ''))
      end

      it "provides debug logging of unparsable lines" do
        parser_test('an unexpected dpkg msg with an exit code of 0', package_not_found_hash.merge(:ensure => :absent), 1)
      end

      it "does not log if execution returns with non-zero exit code" do
        Puppet::Util::Execution.expects(:execute).with(query_args, execute_options).raises Puppet::ExecutionFailure.new("failed")
        Puppet::expects(:debug).never

        expect(provider.query).to eq(package_not_found_hash)
      end
    end
  end

  context "when installing" do
    before do
      resource.stubs(:[]).with(:source).returns "mypkg"
    end

    it "fails to install if no source is specified in the resource" do
      resource.expects(:[]).with(:source).returns nil

      expect { provider.install }.to raise_error(ArgumentError)
    end

    it "uses 'dpkg -i' to install the package" do
      resource.expects(:[]).with(:source).returns "mypackagefile"
      provider.expects(:unhold)
      provider.expects(:dpkg).with { |*command| command[-1] == "mypackagefile"  and command[-2] == "-i" }

      provider.install
    end

    it "keeps old config files if told to do so" do
      resource.expects(:[]).with(:configfiles).returns :keep
      provider.expects(:unhold)
      provider.expects(:dpkg).with { |*command| command[0] == "--force-confold" }

      provider.install
    end

    it "replaces old config files if told to do so" do
      resource.expects(:[]).with(:configfiles).returns :replace
      provider.expects(:unhold)
      provider.expects(:dpkg).with { |*command| command[0] == "--force-confnew" }

      provider.install
    end

    it "ensures any hold is removed" do
      provider.expects(:unhold).once
      provider.expects(:dpkg)
      provider.install
    end
  end

  context "when holding or unholding" do
    let(:tempfile) { stub 'tempfile', :print => nil, :close => nil, :flush => nil, :path => "/other/file" }

    before do
      tempfile.stubs(:write)
      Tempfile.stubs(:open).yields tempfile
    end

    it "installs first if holding" do
      provider.stubs(:execute)
      provider.expects(:install).once
      provider.hold
    end

    it "executes dpkg --set-selections when holding" do
      provider.stubs(:install)
      provider.expects(:execute).with([:dpkg, '--set-selections'], {:failonfail => false, :combine => false, :stdinfile => tempfile.path}).once
      provider.hold
    end

    it "executes dpkg --set-selections when unholding" do
      provider.stubs(:install)
      provider.expects(:execute).with([:dpkg, '--set-selections'], {:failonfail => false, :combine => false, :stdinfile => tempfile.path}).once
      provider.hold
    end
  end

  it "uses :install to update" do
    provider.expects(:install)
    provider.update
  end

  context "when determining latest available version" do
    it "returns the version found by dpkg-deb" do
      resource.expects(:[]).with(:source).returns "myfile"
      provider.expects(:dpkg_deb).with { |*command| command[-1] == "myfile" }.returns "package\t1.0"
      expect(provider.latest).to eq("1.0")
    end

    it "warns if the package file contains a different package" do
      provider.expects(:dpkg_deb).returns("foo\tversion")
      provider.expects(:warning)
      provider.latest
    end

    it "copes with names containing ++" do
      resource = stub 'resource', :[] => "package++"
      provider = described_class.new(resource)
      provider.expects(:dpkg_deb).returns "package++\t1.0"
      expect(provider.latest).to eq("1.0")
    end
  end

  it "uses 'dpkg -r' to uninstall" do
    provider.expects(:dpkg).with("-r", resource_name)
    provider.uninstall
  end

  it "uses 'dpkg --purge' to purge" do
    provider.expects(:dpkg).with("--purge", resource_name)
    provider.purge
  end
end