require 'spec_helper'
require 'r10k/forge/module_release'
require 'r10k/util/exec_env'
require 'puppet_forge'

describe R10K::Forge::ModuleRelease do
  subject { described_class.new('branan-eight_hundred', '8.0.0') }

  let(:forge_release_class) { PuppetForge::V3::Release }
  let(:sha256_digest_class) { Digest::SHA256 }
  let(:md5_digest_class) { Digest::MD5 }

  let(:download_path) { instance_double('Pathname') }
  let(:tarball_cache_path) { instance_double('Pathname') }
  let(:tarball_cache_root) { instance_double('Pathname') }
  let(:unpack_path) { instance_double('Pathname') }
  let(:target_dir) { instance_double('Pathname') }
  let(:tarball_cache_path) { instance_double('Pathname') }
  let(:md5_file_path) { instance_double('Pathname') }
  let(:sha256_file_path) { instance_double('Pathname') }

  let(:file_lists) { {:valid=>['valid_ex'], :invalid=>[], :symlinks=>['symlink_ex']} }

  let(:file_contents) { "skeletor's closet" }
  let(:sha256_digest) { instance_double('Digest::SHA256') }
  let(:sha256_of_tarball) { "sha256_hash" }
  let(:md5_digest) { instance_double('Digest::MD5') }
  let(:md5_of_tarball) { "md5_hash" }
  let(:good_md5) { md5_of_tarball }
  let(:good_sha256) { sha256_of_tarball }
  let(:bad_sha256) { "bad_sha256_hash" }
  let(:bad_md5) { "bad_md5_hash" }

  before do
    subject.download_path = download_path
    subject.tarball_cache_path = tarball_cache_path
    subject.tarball_cache_root = tarball_cache_root
    subject.unpack_path = unpack_path
    subject.md5_file_path = md5_file_path
    subject.sha256_file_path = sha256_file_path
  end

  context "no cached tarball" do
    describe '#download' do
      it "downloads the module from the forge into `download_path`" do
        expect(tarball_cache_path).to receive(:exist?).and_return(false)
        expect(subject.forge_release).to receive(:download).with(download_path)
        allow(FileUtils).to receive(:mkdir_p).with(tarball_cache_root)
        expect(FileUtils).to receive(:mv).with(download_path, tarball_cache_path)
        subject.download
      end
    end
  end

  context "with cached tarball" do
    describe '#download' do
      it "does not download a new tarball" do
        expect(tarball_cache_path).to receive(:exist?).and_return(true)
        expect(subject.forge_release).not_to receive(:download).with(download_path)
        subject.download
      end
    end
  end

  describe '#verify' do

    it "verifies using the file SHA256, if that exists" do
      allow(sha256_digest_class).to receive(:file).and_return(sha256_digest)
      allow(sha256_digest).to receive(:hexdigest).and_return(sha256_of_tarball)
      allow(sha256_file_path).to receive(:exist?).and_return(true)
      expect(subject).to receive(:verify_from_file).with(sha256_of_tarball, sha256_file_path)
      subject.verify
    end

    it "verifies using the forge file_sha256, if no sha256 file exists" do
      allow(sha256_digest_class).to receive(:file).and_return(sha256_digest)
      allow(sha256_digest).to receive(:hexdigest).and_return(sha256_of_tarball)
      allow(sha256_file_path).to receive(:exist?).and_return(false)
      allow(subject.forge_release).to receive(:respond_to?).and_return(true)
      allow(subject.forge_release).to receive(:sha256_file).and_return(sha256_of_tarball)
      expect(subject).to receive(:verify_from_forge)
      subject.verify
    end

    it "falls back to md5 verification when not in FIPS mode and no sha256 available" do
      expect(R10K::Util::Platform).to receive(:fips?).and_return(false)
      # failed sha256 verification
      allow(sha256_digest_class).to receive(:file).and_return(sha256_digest)
      allow(sha256_digest).to receive(:hexdigest).and_return(sha256_of_tarball)
      allow(sha256_file_path).to receive(:exist?).and_return(false)
      allow(subject.forge_release).to receive(:respond_to?).and_return(false)
      allow(subject).to receive(:verify_from_forge)
      # md5 verification
      allow(md5_digest_class).to receive(:file).and_return(md5_digest)
      allow(md5_digest).to receive(:hexdigest).and_return(md5_of_tarball)
      allow(md5_file_path).to receive(:exist?).and_return(true)
      expect(subject).to receive(:verify_from_file)
      subject.verify
    end

    it "errors when in FIPS mode and no sha256 is available" do
      expect(R10K::Util::Platform).to receive(:fips?).and_return(true)
      allow(sha256_digest_class).to receive(:file).and_return(sha256_digest)
      allow(sha256_digest).to receive(:hexdigest).and_return(sha256_of_tarball)
      allow(sha256_file_path).to receive(:exist?).and_return(false)
      allow(subject.forge_release).to receive(:respond_to?).and_return(false)
      allow(subject).to receive(:verify_from_forge)
      expect { subject.verify }.to raise_error(R10K::Error)
    end
  end

  describe '#verify_from_file' do

    it "does nothing when the checksums match" do
      expect(File).to receive(:read).with(sha256_file_path).and_return(good_sha256)
      expect(subject).not_to receive(:cleanup_cached_tarball_path)
      subject.verify_from_file(sha256_of_tarball, sha256_file_path)
    end

    it "raises an error and cleans up when the checksums do not match" do
      expect(File).to receive(:read).with(sha256_file_path).and_return(bad_sha256)
      expect(tarball_cache_path).to receive(:delete)
      expect(sha256_file_path).to receive(:delete)
      expect { subject.verify_from_file(sha256_of_tarball, sha256_file_path) }.to raise_error(PuppetForge::V3::Release::ChecksumMismatch)
    end
  end

  describe '#verify_from_forge' do
    it "write the checksum to file when the checksums match" do
      expect(tarball_cache_path).not_to receive(:delete)
      expect(File).to receive(:write).with(sha256_file_path, good_sha256)
      subject.verify_from_forge(sha256_of_tarball, good_sha256, sha256_file_path)
    end

    it "raises an error and cleans up when the checksums do not match" do
      expect(tarball_cache_path).to receive(:delete)
      expect { subject.verify_from_forge(sha256_of_tarball, bad_sha256, sha256_file_path) }
        .to raise_error(PuppetForge::V3::Release::ChecksumMismatch)
    end
  end

  describe '#unpack' do
    it "unpacks the module tarball in `tarball_cache_path` into the provided target path" do
      expect(PuppetForge::Unpacker).to receive(:unpack).with(tarball_cache_path.to_s, target_dir.to_s, unpack_path.to_s).\
          and_return({:valid=>["extractedmodule/metadata.json"], :invalid=>[], :symlinks=>[]})
      subject.unpack(target_dir)
    end

    it "logs a warning if symlinks are present during the unpacking process" do
      logger_dbl = double(Log4r::Logger, debug1: true, debug2: true)
      allow(subject).to receive(:logger).and_return(logger_dbl)
      allow(PuppetForge::Unpacker).to receive(:unpack).and_return(file_lists)

      expect(logger_dbl).to receive(:warn).with(/symlinks are unsupported.*#{Regexp.escape(subject.forge_release.slug)}/i)

      subject.unpack(target_dir)
    end
  end

  describe "#cleanup" do
    it "cleans up the unpack paths" do
      expect(subject).to receive(:cleanup_unpack_path)
      expect(subject).to receive(:cleanup_download_path)
      subject.cleanup
    end
  end

  describe "#cleanup_unpack_path" do
    it "ignores the unpack_path if the parent doesn't exist" do
      parent = instance_double('Pathname')
      expect(parent).to receive(:exist?).and_return false
      expect(parent).to_not receive(:rmtree)
      expect(unpack_path).to receive(:parent).and_return(parent)
      subject.cleanup_unpack_path
    end

    it "removes the containing directory of unpack_path if it exists" do
      parent = instance_double('Pathname')
      expect(parent).to receive(:rmtree)
      expect(parent).to receive(:exist?).and_return true
      expect(unpack_path).to receive(:parent).and_return(parent).exactly(2).times
      subject.cleanup_unpack_path
    end
  end

  describe "#cleanup_download_path" do
    it "ignores the download_path if the parent doesn't exist" do
      parent = instance_double('Pathname')
      expect(parent).to receive(:exist?).and_return false
      expect(parent).to_not receive(:rmtree)
      expect(download_path).to receive(:parent).and_return(parent)
      subject.cleanup_download_path
    end

    it "removes the containing directory of download_path if it exists" do
      parent = instance_double('Pathname')
      expect(parent).to receive(:rmtree)
      expect(parent).to receive(:exist?).and_return true
      expect(download_path).to receive(:parent).and_return(parent).exactly(2).times
      subject.cleanup_download_path
    end
  end

  describe '#install' do
    it "performs all steps needed to install the module" do
      expect(subject).to receive(:download)
      expect(subject).to receive(:verify)
      expect(subject).to receive(:unpack).with(target_dir)
      expect(subject).to receive(:cleanup)
      subject.install(target_dir)
    end
  end
end
