require 'spec_helper'

describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
  include TrackUntrackedUploadsHelpers

  let!(:untracked_files_for_uploads) { described_class::UntrackedFile }

  matcher :be_scheduled_migration do |*expected|
    match do |migration|
      BackgroundMigrationWorker.jobs.any? do |job|
        job['args'] == [migration, expected]
      end
    end

    failure_message do |migration|
      "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
    end
  end

  before do
    DatabaseCleaner.clean

    drop_temp_table_if_exists
  end

  after do
    drop_temp_table_if_exists
  end

  around do |example|
    # Especially important so the follow-up migration does not get run
    Sidekiq::Testing.fake! do
      example.run
    end
  end

  it 'ensures the untracked_files_for_uploads table exists' do
    expect do
      described_class.new.perform
    end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true)
  end

  it 'has a path field long enough for really long paths' do
    described_class.new.perform

    component = 'a' * 255

    long_path = [
      'uploads',
      component, # project.full_path
      component  # filename
    ].flatten.join('/')

    record = untracked_files_for_uploads.create!(path: long_path)
    expect(record.reload.path.size).to eq(519)
  end

  context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do
    around do |example|
      # If this is CI, we use Postgres 9.2 so this whole context should be
      # skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
      if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?)
        example.run
      end
    end

    context 'when files were uploaded before and after hashed storage was enabled' do
      let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
      let!(:user) { create(:user, :with_avatar) }
      let!(:project1) { create(:project, :with_avatar) }
      let(:project2) { create(:project) } # instantiate after enabling hashed_storage

      before do
        # Markdown upload before enabling hashed_storage
        UploadService.new(project1, uploaded_file, FileUploader).execute

        stub_application_setting(hashed_storage_enabled: true)

        # Markdown upload after enabling hashed_storage
        UploadService.new(project2, uploaded_file, FileUploader).execute
      end

      it 'adds unhashed files to the untracked_files_for_uploads table' do
        described_class.new.perform

        expect(untracked_files_for_uploads.count).to eq(5)
      end

      it 'adds files with paths relative to CarrierWave.root' do
        described_class.new.perform
        untracked_files_for_uploads.all.each do |file|
          expect(file.path.start_with?('uploads/')).to be_truthy
        end
      end

      it 'does not add hashed files to the untracked_files_for_uploads table' do
        described_class.new.perform

        hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
        expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
      end

      it 'correctly schedules the follow-up background migration jobs' do
        described_class.new.perform

        expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
        expect(BackgroundMigrationWorker.jobs.size).to eq(1)
      end

      # E.g. from a previous failed run of this background migration
      context 'when there is existing data in untracked_files_for_uploads' do
        before do
          described_class.new.perform
        end

        it 'does not error or produce duplicates of existing data' do
          expect do
            described_class.new.perform
          end.not_to change { untracked_files_for_uploads.count }.from(5)
        end
      end

      # E.g. The installation is in use at the time of migration, and someone has
      # just uploaded a file
      context 'when there are files in /uploads/tmp' do
        let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }

        before do
          FileUtils.touch(tmp_file)
        end

        after do
          FileUtils.rm(tmp_file)
        end

        it 'does not add files from /uploads/tmp' do
          described_class.new.perform

          expect(untracked_files_for_uploads.count).to eq(5)
        end
      end
    end
  end

  context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
    before do
      # If this is CI, we use Postgres 9.2 so this stub has no effect.
      #
      # If this is being run on Postgres 9.5+ or MySQL, then this stub allows us
      # to test the bulk insert functionality without ON CONFLICT DO NOTHING or
      # IGNORE.
      allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
    end

    context 'when files were uploaded before and after hashed storage was enabled' do
      let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
      let!(:user) { create(:user, :with_avatar) }
      let!(:project1) { create(:project, :with_avatar) }
      let(:project2) { create(:project) } # instantiate after enabling hashed_storage

      before do
        # Markdown upload before enabling hashed_storage
        UploadService.new(project1, uploaded_file, FileUploader).execute

        stub_application_setting(hashed_storage_enabled: true)

        # Markdown upload after enabling hashed_storage
        UploadService.new(project2, uploaded_file, FileUploader).execute
      end

      it 'adds unhashed files to the untracked_files_for_uploads table' do
        described_class.new.perform

        expect(untracked_files_for_uploads.count).to eq(5)
      end

      it 'adds files with paths relative to CarrierWave.root' do
        described_class.new.perform
        untracked_files_for_uploads.all.each do |file|
          expect(file.path.start_with?('uploads/')).to be_truthy
        end
      end

      it 'does not add hashed files to the untracked_files_for_uploads table' do
        described_class.new.perform

        hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
        expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
      end

      it 'correctly schedules the follow-up background migration jobs' do
        described_class.new.perform

        expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
        expect(BackgroundMigrationWorker.jobs.size).to eq(1)
      end

      # E.g. from a previous failed run of this background migration
      context 'when there is existing data in untracked_files_for_uploads' do
        before do
          described_class.new.perform
        end

        it 'does not error or produce duplicates of existing data' do
          expect do
            described_class.new.perform
          end.not_to change { untracked_files_for_uploads.count }.from(5)
        end
      end

      # E.g. The installation is in use at the time of migration, and someone has
      # just uploaded a file
      context 'when there are files in /uploads/tmp' do
        let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }

        before do
          FileUtils.touch(tmp_file)
        end

        after do
          FileUtils.rm(tmp_file)
        end

        it 'does not add files from /uploads/tmp' do
          described_class.new.perform

          expect(untracked_files_for_uploads.count).to eq(5)
        end
      end
    end
  end

  # Very new or lightly-used installations that are running this migration
  # may not have an upload directory because they have no uploads.
  context 'when no files were ever uploaded' do
    it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do
      described_class.new.perform

      expect(untracked_files_for_uploads.count).to eq(0)
    end
  end
end
