require 'spec_helper'

load Metasploit::Framework.root.join('msfupdate').to_path

RSpec.describe Msfupdate do

  def dummy_pathname
    Pathname.new(File.dirname(__FILE__)).join('dummy')
  end

  def dummy_git_pathname
    dummy_pathname.join('gitbase')
  end

  def dummy_install_pathname
    dummy_pathname.join('installbase').join('msf3')
  end

  def dummy_apt_pathname
    dummy_pathname.join('aptbase')
  end

  let(:msfbase_dir) do
    dummy_git_pathname
  end

  let(:stdin)  { StringIO.new("", "rb") }
  let(:stdout) { StringIO.new("", "wb") }
  let(:stderr) { StringIO.new("", "wb") }

  subject do
    Msfupdate.new(msfbase_dir, stdin, stdout, stderr)
  end

  before(:context) do
    # Create some fake directories to mock our different install environments
    dummy_pathname.mkpath
    dummy_apt_pathname.join('.apt').mkpath
    dummy_git_pathname.join('.git').mkpath
    dummy_install_pathname.mkpath
    dummy_install_pathname.join('..', 'engine').mkpath
    FileUtils.touch(dummy_install_pathname.join('..', 'engine', 'update.rb'))
  end

  after(:context) do
    dummy_pathname.rmtree
  end

  before(:example) do
    # By default, we want to ensure tests never actually try to execute any
    # of the update methods unless we are explicitly testing them
    allow(subject).to receive(:update_apt!)
    allow(subject).to receive(:update_binary_install!)
    allow(subject).to receive(:update_git!)
  end

  context "#parse_args" do
    it "doesn't alter ARGV" do
      ARGV.clear
      ARGV << 'foo'
      subject.parse_args(['x', 'y'])
      expect(ARGV).to eq ['foo']
    end

    context "with --help" do
      let(:args) { ['--help'] }

      it "calls usage" do
        expect(subject).to receive(:usage)
        begin
          subject.parse_args(args)
        rescue SystemExit
        end
      end

      it "exits before updating" do
        expect {subject.parse_args(args)}.to raise_error(SystemExit)
      end
    end

    context "with --git-branch" do
      let(:git_branch) { 'foo' }
      let(:args) { ['--git-branch', git_branch] }

      it "sets @git_branch" do
        subject.parse_args(args)
        expect(subject.instance_variable_get(:@git_branch)).to eq git_branch
      end

      context "without a space" do
        let(:args) { ["--git-branch=#{git_branch}"] }

        it "sets @git_branch" do
          subject.parse_args(args)
          expect(subject.instance_variable_get(:@git_branch)).to eq git_branch
        end
      end
    end

    context "with --git-remote" do
      let(:git_remote) { 'foo' }
      let(:args) { ['--git-remote', git_remote] }

      it "sets @git_remote" do
        subject.parse_args(args)
        expect(subject.instance_variable_get(:@git_remote)).to eq git_remote
      end

      context "without a space" do
        let(:args) { ["--git-remote=#{git_remote}"] }

        it "sets @git_remote" do
          subject.parse_args(args)
          expect(subject.instance_variable_get(:@git_remote)).to eq git_remote
        end
      end
    end

    context "with --offline-file" do
      let(:offline_file) { 'foo' }
      let(:args) { ['--offline-file', offline_file] }

      it "sets @offline_file" do
        subject.parse_args(args)
        expect(subject.instance_variable_get(:@offline_file)).to match Regexp.new(Regexp.escape(offline_file))
      end

      context "with relative path" do
        it "transforms argument into an absolute path" do
          subject.parse_args(args)
          expect(subject.instance_variable_get(:@offline_file)).to eq File.join(Dir.pwd, offline_file)
        end
      end

      context "with absolute path" do
        let(:offline_file) { '/tmp/foo' }
        it "accepts an absolute path" do
          subject.parse_args(args)
          expect(subject.instance_variable_get(:@offline_file)).to eq offline_file
        end
      end

      context "without a space" do
        let(:args) { ["--offline-file=#{offline_file}"] }

        it "sets @offline_file" do
          subject.parse_args(["--offline-file=#{offline_file}"])
          expect(subject.instance_variable_get(:@offline_file)).to match Regexp.new(Regexp.escape(offline_file))
        end
      end
    end

    context "with wait" do
      let(:args) { ['wait'] }
      it "sets @actually_wait" do
        subject.parse_args(args)
        expect(subject.instance_variable_get(:@actually_wait)).to eq true
      end
    end

    context "with nowait" do
      let(:args) { ['nowait'] }
      it "sets @actually_wait" do
        subject.parse_args(args)
        expect(subject.instance_variable_get(:@actually_wait)).to eq false
      end
    end
  end

  context "#run!" do
    before(:example) do
      subject.parse_args(args)
    end
    let(:args) { [] }

    it "calls validate_args" do
      expect(subject).to receive(:validate_args) { true }
      subject.run!
    end

    it "exits if arguments are invalid" do
      allow(subject).to receive(:validate_args).and_return(false)
      expect(subject).to receive(:maybe_wait_and_exit).and_raise(SystemExit)
      expect { subject.run! }.to raise_error(SystemExit)
    end
  end

  context "in an apt installation" do
    let(:msfbase_dir) { dummy_apt_pathname }

    it { expect(subject.apt?).to be_truthy }
    it { expect(subject.binary_install?).to be_falsey }
    it { expect(subject.git?).to be_falsey }

    context "#validate_args" do
      before(:example) do
        subject.parse_args(args)
      end

      context "with no args" do
        let(:args) { [] }
        it { expect(subject.validate_args).to be_truthy }
      end

      context "with --git-remote" do
        let(:args) { ['--git-remote', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end

      context "with --git-branch" do
        let(:args) { ['--git-branch', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end

      context "with --offline-file" do
        let(:args) { ['--offline-file', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end
    end

    context "#run!" do
      it "calls update_apt!" do
        expect(subject).to receive(:update_apt!)
        subject.run!
      end
      it "does not call update_binary_install!" do
        expect(subject).not_to receive(:update_binary_install!)
        subject.run!
      end
      it "does not call update_git!" do
        expect(subject).not_to receive(:update_git!)
        subject.run!
      end
    end

    context "#update_apt!" do
      # TODO: Add more tests!
    end
  end

  context "in a binary installation" do
    let(:msfbase_dir) { dummy_install_pathname }

    it { expect(subject.apt?).to be_falsey }
    it { expect(subject.binary_install?).to be_truthy }
    it { expect(subject.git?).to be_falsey }

    context "#validate_args" do
      before(:example) do
        subject.parse_args(args)
      end

      context "with no args" do
        let(:args) { [] }
        it { expect(subject.validate_args).to be_truthy }
      end

      context "with --git-remote" do
        let(:args) { ['--git-remote', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end

      context "with --git-branch" do
        let(:args) { ['--git-branch', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end

      context "with --offline-file" do
        let(:args) { ['--offline-file', 'foo'] }
        it { expect(subject.validate_args).to be_truthy }
      end
    end

    context "#run!" do
      it "does not call update_apt!" do
        expect(subject).not_to receive(:update_apt!)
        subject.run!
      end
      it "calls update_binary_install!" do
        expect(subject).to receive(:update_binary_install!)
        subject.run!
      end
      it "does not call update_git!" do
        expect(subject).not_to receive(:update_git!)
        subject.run!
      end
    end

    context "#update_binary_install!" do
      # TODO: Add more tests!
    end
  end

  context "in a git installation" do
    let(:msfbase_dir) { dummy_git_pathname }

    it { expect(subject.apt?).to be_falsey }
    it { expect(subject.binary_install?).to be_falsey }
    it { expect(subject.git?).to be_truthy }


    context "#validate_args" do
      before(:example) do
        subject.parse_args(args)
      end

      context "with no args" do
        let(:args) { [] }
        it { expect(subject.validate_args).to be_truthy }
      end

      context "with --git-remote" do
        let(:args) { ['--git-remote', 'foo'] }
        it { expect(subject.validate_args).to be_truthy }
      end

      context "with --git-branch" do
        let(:args) { ['--git-branch', 'foo'] }
        it { expect(subject.validate_args).to be_truthy }
      end

      context "with --offline-file" do
        let(:args) { ['--offline-file', 'foo'] }
        it { expect(subject.validate_args).to be_falsey }
      end
    end

    context "#run!" do
      it "does not call update_apt!" do
        expect(subject).not_to receive(:update_apt!)
        subject.run!
      end
      it "does not call update_binary_install!" do
        expect(subject).not_to receive(:update_binary_install!)
        subject.run!
      end
      it "calls update_git!" do
        expect(subject).to receive(:update_git!)
        subject.run!
      end
    end

    context "#update_git!" do
      # TODO: Add more tests!
    end
  end

end
