require 'spec_helper'

describe Backup::Manager do
  include StubENV

  let(:progress) { StringIO.new }

  subject { described_class.new(progress) }

  before do
    allow(progress).to receive(:puts)
    allow(progress).to receive(:print)

    allow_any_instance_of(String).to receive(:color) do |string, _color|
      string
    end

    @old_progress = $progress # rubocop:disable Style/GlobalVars
    $progress = progress # rubocop:disable Style/GlobalVars
  end

  after do
    $progress = @old_progress # rubocop:disable Style/GlobalVars
  end

  describe '#remove_old' do
    let(:files) do
      [
        '1451606400_2016_01_01_1.2.3_gitlab_backup.tar',
        '1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
        '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar',
        '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar',
        '1451520000_2015_12_31_4.5.6-pre-ee_gitlab_backup.tar',
        '1451510000_2015_12_30_gitlab_backup.tar',
        '1450742400_2015_12_22_gitlab_backup.tar',
        '1449878400_gitlab_backup.tar',
        '1449014400_gitlab_backup.tar',
        'manual_gitlab_backup.tar'
      ]
    end

    before do
      allow(Dir).to receive(:chdir).and_yield
      allow(Dir).to receive(:glob).and_return(files)
      allow(FileUtils).to receive(:rm)
      allow(Time).to receive(:now).and_return(Time.utc(2016))
    end

    context 'when keep_time is zero' do
      before do
        allow(Gitlab.config.backup).to receive(:keep_time).and_return(0)

        subject.remove_old
      end

      it 'removes no files' do
        expect(FileUtils).not_to have_received(:rm)
      end

      it 'prints a skipped message' do
        expect(progress).to have_received(:puts).with('skipping')
      end
    end

    context 'when no valid file is found' do
      let(:files) do
        [
          '14516064000_2016_01_01_1.2.3_gitlab_backup.tar',
          'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
          '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar'
        ]
      end

      before do
        allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)

        subject.remove_old
      end

      it 'removes no files' do
        expect(FileUtils).not_to have_received(:rm)
      end

      it 'prints a done message' do
        expect(progress).to have_received(:puts).with('done. (0 removed)')
      end
    end

    context 'when there are no files older than keep_time' do
      before do
        # Set to 30 days
        allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000)

        subject.remove_old
      end

      it 'removes no files' do
        expect(FileUtils).not_to have_received(:rm)
      end

      it 'prints a done message' do
        expect(progress).to have_received(:puts).with('done. (0 removed)')
      end
    end

    context 'when keep_time is set to remove files' do
      before do
        # Set to 1 second
        allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)

        subject.remove_old
      end

      it 'removes matching files with a human-readable versioned timestamp' do
        expect(FileUtils).to have_received(:rm).with(files[1])
        expect(FileUtils).to have_received(:rm).with(files[2])
        expect(FileUtils).to have_received(:rm).with(files[3])
      end

      it 'removes matching files with a human-readable versioned timestamp with tagged EE' do
        expect(FileUtils).to have_received(:rm).with(files[4])
      end

      it 'removes matching files with a human-readable non-versioned timestamp' do
        expect(FileUtils).to have_received(:rm).with(files[5])
        expect(FileUtils).to have_received(:rm).with(files[6])
      end

      it 'removes matching files without a human-readable timestamp' do
        expect(FileUtils).to have_received(:rm).with(files[7])
        expect(FileUtils).to have_received(:rm).with(files[8])
      end

      it 'does not remove files that are not old enough' do
        expect(FileUtils).not_to have_received(:rm).with(files[0])
      end

      it 'does not remove non-matching files' do
        expect(FileUtils).not_to have_received(:rm).with(files[9])
      end

      it 'prints a done message' do
        expect(progress).to have_received(:puts).with('done. (8 removed)')
      end
    end

    context 'when removing a file fails' do
      let(:file) { files[1] }
      let(:message) { "Permission denied @ unlink_internal - #{file}" }

      before do
        allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
        allow(FileUtils).to receive(:rm).with(file).and_raise(Errno::EACCES, message)

        subject.remove_old
      end

      it 'removes the remaining expected files' do
        expect(FileUtils).to have_received(:rm).with(files[4])
        expect(FileUtils).to have_received(:rm).with(files[5])
        expect(FileUtils).to have_received(:rm).with(files[6])
        expect(FileUtils).to have_received(:rm).with(files[7])
        expect(FileUtils).to have_received(:rm).with(files[8])
      end

      it 'sets the correct removed count' do
        expect(progress).to have_received(:puts).with('done. (7 removed)')
      end

      it 'prints the error from file that could not be removed' do
        expect(progress).to have_received(:puts).with(a_string_matching(message))
      end
    end
  end

  describe '#unpack' do
    context 'when there are no backup files in the directory' do
      before do
        allow(Dir).to receive(:glob).and_return([])
      end

      it 'fails the operation and prints an error' do
        expect { subject.unpack }.to raise_error SystemExit
        expect(progress).to have_received(:puts)
          .with(a_string_matching('No backups found'))
      end
    end

    context 'when there are two backup files in the directory and BACKUP variable is not set' do
      before do
        allow(Dir).to receive(:glob).and_return(
          [
            '1451606400_2016_01_01_1.2.3_gitlab_backup.tar',
            '1451520000_2015_12_31_gitlab_backup.tar'
          ]
        )
      end

      it 'prints the list of available backups' do
        expect { subject.unpack }.to raise_error SystemExit
        expect(progress).to have_received(:puts)
          .with(a_string_matching('1451606400_2016_01_01_1.2.3\n 1451520000_2015_12_31'))
      end

      it 'fails the operation and prints an error' do
        expect { subject.unpack }.to raise_error SystemExit
        expect(progress).to have_received(:puts)
          .with(a_string_matching('Found more than one backup'))
      end
    end

    context 'when BACKUP variable is set to a non-existing file' do
      before do
        allow(Dir).to receive(:glob).and_return(
          [
            '1451606400_2016_01_01_gitlab_backup.tar'
          ]
        )
        allow(File).to receive(:exist?).and_return(false)

        stub_env('BACKUP', 'wrong')
      end

      it 'fails the operation and prints an error' do
        expect { subject.unpack }.to raise_error SystemExit
        expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar')
        expect(progress).to have_received(:puts)
          .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist'))
      end
    end

    context 'when BACKUP variable is set to a correct file' do
      before do
        allow(Dir).to receive(:glob).and_return(
          [
            '1451606400_2016_01_01_1.2.3_gitlab_backup.tar'
          ]
        )
        allow(File).to receive(:exist?).and_return(true)
        allow(Kernel).to receive(:system).and_return(true)
        allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION)

        stub_env('BACKUP', '1451606400_2016_01_01_1.2.3')
      end

      it 'unpacks the file' do
        subject.unpack

        expect(Kernel).to have_received(:system)
          .with("tar", "-xf", "1451606400_2016_01_01_1.2.3_gitlab_backup.tar")
        expect(progress).to have_received(:puts).with(a_string_matching('done'))
      end
    end
  end

  describe '#upload' do
    let(:backup_file) { Tempfile.new('backup', Gitlab.config.backup.path) }
    let(:backup_filename) { File.basename(backup_file.path) }

    before do
      allow(subject).to receive(:tar_file).and_return(backup_filename)

      stub_backup_setting(
        upload: {
          connection: {
            provider: 'AWS',
            aws_access_key_id: 'id',
            aws_secret_access_key: 'secret'
          },
          remote_directory: 'directory',
          multipart_chunk_size: 104857600,
          encryption: nil,
          storage_class: nil
        }
      )

      Fog.mock!

      # the Fog mock only knows about directories we create explicitly
      connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys)
      connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
    end

    context 'target path' do
      it 'uses the tar filename by default' do
        expect_any_instance_of(Fog::Collection).to receive(:create)
          .with(hash_including(key: backup_filename))
          .and_return(true)

        Dir.chdir(Gitlab.config.backup.path) do
          subject.upload
        end
      end

      it 'adds the DIRECTORY environment variable if present' do
        stub_env('DIRECTORY', 'daily')

        expect_any_instance_of(Fog::Collection).to receive(:create)
          .with(hash_including(key: "daily/#{backup_filename}"))
          .and_return(true)

        Dir.chdir(Gitlab.config.backup.path) do
          subject.upload
        end
      end
    end
  end
end
