Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class OverwriteProjectService < BaseService
    def execute(source_project)
      return unless source_project && source_project.namespace_id == @project.namespace_id

      start_time = ::Gitlab::Metrics::System.monotonic_time
      original_source_name = source_project.name
      original_source_path = source_project.path
      tmp_source_name, tmp_source_path = tmp_source_project_name(source_project)

      move_relationships_between(source_project, @project)

      source_project_rename = rename_project(source_project, tmp_source_name, tmp_source_path)

      if source_project_rename[:status] == :error
        raise 'Source project rename failed during project overwrite'
      end

      new_project_rename = rename_project(@project, original_source_name, original_source_path)

      if new_project_rename[:status] == :error
        rename_project(source_project, original_source_name, original_source_path)

        raise 'New project rename failed during project overwrite'
      end

      schedule_source_project_deletion(source_project)

      @project
    rescue StandardError => e
      move_relationships_between(@project, source_project)
      remove_source_project_from_fork_network(source_project)

      raise e
    ensure
      track_service(start_time, source_project, e)
    end

    private

    def track_service(start_time, source_project, exception)
      return if ::Feature.disabled?(:project_overwrite_service_tracking, source_project)

      duration = ::Gitlab::Metrics::System.monotonic_time - start_time

      Gitlab::AppJsonLogger.info(
        class: self.class.name,
        namespace_id: source_project.namespace_id,
        project_id: source_project.id,
        duration_s: duration.to_f,
        error: exception.class.name
      )
    end

    def move_relationships_between(source_project, target_project)
      options = { remove_remaining_elements: false }

      Project.transaction do
        ::Projects::MoveUsersStarProjectsService.new(target_project, @current_user).execute(source_project, **options)
        ::Projects::MoveAccessService.new(target_project, @current_user).execute(source_project, **options)
        ::Projects::MoveDeployKeysProjectsService.new(target_project, @current_user).execute(source_project, **options)
        ::Projects::MoveNotificationSettingsService.new(target_project, @current_user).execute(source_project, **options)
        ::Projects::MoveForksService.new(target_project, @current_user).execute(source_project, **options)
        ::Projects::MoveLfsObjectsProjectsService.new(target_project, @current_user).execute(source_project, **options)

        add_source_project_to_fork_network(source_project)
      end
    end

    def schedule_source_project_deletion(source_project)
      ::Projects::DestroyService.new(source_project, @current_user).async_execute
    end

    def rename_project(target_project, name, path)
      ::Projects::UpdateService.new(target_project, @current_user, { name: name, path: path }).execute
    end

    def add_source_project_to_fork_network(source_project)
      return if source_project == @project
      return unless fork_network

      # Because they have moved all references in the fork network from the source_project
      # we won't be able to query the database (only through its cached data),
      # for its former relationships. That's why we're adding it to the network
      # as a fork of the target project
      ForkNetworkMember.create!(
        fork_network: fork_network,
        project: source_project,
        forked_from_project: @project
      )
    end

    def remove_source_project_from_fork_network(source_project)
      return unless fork_network

      fork_member = ForkNetworkMember.find_by( # rubocop: disable CodeReuse/ActiveRecord
        fork_network: fork_network,
        project: source_project,
        forked_from_project: @project)

      fork_member&.destroy
    end

    def tmp_source_project_name(source_project)
      random_string = SecureRandom.hex
      tmp_name = "#{source_project.name}-old-#{random_string}"
      tmp_path = "#{source_project.path}-old-#{random_string}"

      [tmp_name, tmp_path]
    end

    def fork_network
      @project.fork_network_member&.fork_network
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::OverwriteProjectService, feature_category: :groups_and_projects do
  include ProjectForksHelper

  let(:user) { create(:user) }
  let(:project_from) { create(:project, namespace: user.namespace) }
  let(:project_to) { create(:project, namespace: user.namespace) }
  let!(:lvl1_forked_project_1) { fork_project(project_from, user) }
  let!(:lvl1_forked_project_2) { fork_project(project_from, user) }
  let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) }
  let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) }

  subject { described_class.new(project_to, user) }

  before do
    project_to.project_feature.reload

    allow(project_to).to receive(:import_data).and_return(double(data: { 'original_path' => project_from.path }))
  end

  describe '#execute' do
    shared_examples 'overwrite actions' do
      it 'moves deploy keys' do
        deploy_keys_count = project_from.deploy_keys_projects.count

        subject.execute(project_from)

        expect(project_to.deploy_keys_projects.count).to eq deploy_keys_count
      end

      it 'moves notification settings' do
        notification_count = project_from.notification_settings.count

        subject.execute(project_from)

        expect(project_to.notification_settings.count).to eq notification_count
      end

      it 'moves users stars' do
        stars_count = project_from.users_star_projects.count

        subject.execute(project_from)
        project_to.reload

        expect(project_to.users_star_projects.count).to eq stars_count
        expect(project_to.star_count).to eq stars_count
      end

      it 'moves project group links' do
        group_links_count = project_from.project_group_links.count

        subject.execute(project_from)

        expect(project_to.project_group_links.count).to eq group_links_count
      end

      it 'moves memberships and authorizations' do
        members_count = project_from.project_members.count
        project_authorizations = project_from.project_authorizations.count

        subject.execute(project_from)

        expect(project_to.project_members.count).to eq members_count
        expect(project_to.project_authorizations.count).to eq project_authorizations
      end

      context 'moves lfs objects relationships' do
        before do
          create_list(:lfs_objects_project, 3, project: project_from)
        end

        it do
          lfs_objects_count = project_from.lfs_objects.count

          subject.execute(project_from)

          expect(project_to.lfs_objects.count).to eq lfs_objects_count
        end
      end

      it 'schedules original project for deletion' do
        expect_next_instance_of(Projects::DestroyService) do |service|
          expect(service).to receive(:async_execute)
        end

        subject.execute(project_from)
      end

      it 'renames the project' do
        original_path = project_from.full_path

        subject.execute(project_from)

        expect(project_to.full_path).to eq(original_path)
      end

      it 'renames source project to temp name' do
        allow(SecureRandom).to receive(:hex).and_return('test')

        subject.execute(project_from)

        expect(project_from.full_path).to include('-old-test')
      end

      context 'when project rename fails' do
        before do
          expect(subject).to receive(:move_relationships_between).with(project_from, project_to)
          expect(subject).to receive(:move_relationships_between).with(project_to, project_from)
        end

        context 'source rename' do
          it 'moves relations back to source project and raises an exception' do
            allow(subject).to receive(:rename_project).and_return(status: :error)

            expect { subject.execute(project_from) }.to raise_error(StandardError, 'Source project rename failed during project overwrite')
          end
        end

        context 'new project rename' do
          it 'moves relations back, renames source project back to original name and raises' do
            name = project_from.name
            path = project_from.path

            allow(subject).to receive(:rename_project).and_call_original
            allow(subject).to receive(:rename_project).with(project_to, name, path).and_return(status: :error)

            expect { subject.execute(project_from) }.to raise_error(StandardError, 'New project rename failed during project overwrite')

            expect(project_from.name).to eq(name)
            expect(project_from.path).to eq(path)
          end
        end
      end
    end

    context 'when project does not have any relation' do
      it_behaves_like 'overwrite actions'
    end

    context 'when project with elements' do
      it_behaves_like 'overwrite actions' do
        let(:maintainer_user) { create(:user) }
        let(:reporter_user) { create(:user) }
        let(:developer_user) { create(:user) }
        let(:maintainer_group) { create(:group) }
        let(:reporter_group) { create(:group) }
        let(:developer_group) { create(:group) }

        before do
          create_list(:deploy_keys_project, 2, project: project_from)
          create_list(:notification_setting, 2, source: project_from)
          create_list(:users_star_project, 2, project: project_from)
          project_from.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
          project_from.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
          project_from.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER)
          project_from.add_maintainer(maintainer_user)
          project_from.add_developer(developer_user)
          project_from.add_reporter(reporter_user)
        end
      end
    end

    context 'forks', :sidekiq_inline do
      context 'when moving a root forked project' do
        it 'moves the descendant forks' do
          expect(project_from.forks.count).to eq 2
          expect(project_to.forks.count).to eq 0

          subject.execute(project_from)

          expect(project_from.forks.count).to eq 0
          expect(project_to.forks.count).to eq 2
          expect(lvl1_forked_project_1.forked_from_project).to eq project_to
          expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq project_to
          expect(lvl1_forked_project_2.forked_from_project).to eq project_to
          expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq project_to
        end

        it 'updates the fork network' do
          expect(project_from.fork_network.root_project).to eq project_from
          expect(project_from.fork_network.fork_network_members.map(&:project)).to include project_from

          subject.execute(project_from)

          expect(project_to.reload.fork_network.root_project).to eq project_to
          expect(project_to.fork_network.fork_network_members.map(&:project)).not_to include project_from
        end
      end

      context 'when moving a intermediate forked project' do
        let(:project_to) { create(:project, namespace: lvl1_forked_project_1.namespace) }

        it 'moves the descendant forks' do
          expect(lvl1_forked_project_1.forks.count).to eq 2
          expect(project_to.forks.count).to eq 0

          subject.execute(lvl1_forked_project_1)

          expect(lvl1_forked_project_1.forks.count).to eq 0
          expect(project_to.forks.count).to eq 2
          expect(lvl2_forked_project_1_1.forked_from_project).to eq project_to
          expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq project_to
          expect(lvl2_forked_project_1_2.forked_from_project).to eq project_to
          expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq project_to
        end

        it 'moves the ascendant fork' do
          subject.execute(lvl1_forked_project_1)

          expect(project_to.reload.forked_from_project).to eq project_from
          expect(project_to.fork_network_member.forked_from_project).to eq project_from
        end

        it 'does not update fork network' do
          subject.execute(lvl1_forked_project_1)

          expect(project_to.reload.fork_network.root_project).to eq project_from
        end
      end
    end

    context 'if an exception is raised' do
      before do
        allow(subject).to receive(:rename_project).and_raise(StandardError)
      end

      it 'rollbacks changes' do
        updated_at = project_from.updated_at

        expect { subject.execute(project_from) }.to raise_error(StandardError)
        expect(Project.find(project_from.id)).not_to be_nil
        expect(project_from.reload.updated_at.change(usec: 0)).to eq updated_at.change(usec: 0)
      end

      it 'removes fork network member' do
        expect(ForkNetworkMember).to receive(:create!)
        expect(ForkNetworkMember).to receive(:find_by)
        expect(subject).to receive(:remove_source_project_from_fork_network).and_call_original

        expect { subject.execute(project_from) }.to raise_error(StandardError)

        expect(project_from.fork_network_member).to be_nil
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class CreateFromTemplateService < BaseService
    include Gitlab::Utils::StrongMemoize

    attr_reader :template_name

    def initialize(user, params)
      @current_user = user
      @params = params.to_h.dup
      @template_name = @params.delete(:template_name).presence
    end

    def execute
      return project unless validate_template!

      file = built_in_template&.file || sample_data_template&.file

      override_params = params.dup

      if built_in_template
        params[:file] = built_in_template.file
      elsif sample_data_template
        params[:file] = sample_data_template.file
        params[:sample_data] = true
      end

      GitlabProjectsImportService.new(current_user, params, override_params).execute
    ensure
      file&.close
    end

    private

    def validate_template!
      return true if built_in_template || sample_data_template

      project.errors.add(:template_name, _("'%{template_name}' is unknown or invalid" % { template_name: template_name }))
      false
    end

    def built_in_template
      strong_memoize(:built_in_template) do
        Gitlab::ProjectTemplate.find(template_name)
      end
    end

    def sample_data_template
      strong_memoize(:sample_data_template) do
        Gitlab::SampleDataTemplate.find(template_name)
      end
    end

    def project
      @project ||= ::Project.new(namespace_id: params[:namespace_id])
    end
  end
end

Projects::CreateFromTemplateService.prepend_mod_with('Projects::CreateFromTemplateService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::CreateFromTemplateService, feature_category: :groups_and_projects do
  let(:user) { create(:user) }
  let(:template_name) { 'rails' }
  let(:project_params) do
    {
        path: user.to_param,
        template_name: template_name,
        description: 'project description',
        visibility_level: Gitlab::VisibilityLevel::PUBLIC
    }
  end

  subject { described_class.new(user, project_params) }

  it 'calls the importer service' do
    import_service_double = double

    allow(Projects::GitlabProjectsImportService).to receive(:new).and_return(import_service_double)
    expect(import_service_double).to receive(:execute)

    subject.execute
  end

  it 'returns the project that is created' do
    project = subject.execute

    expect(project).to be_saved
    expect(project.import_scheduled?).to be(true)
  end

  context 'when template is not present' do
    let(:template_name) { 'non_existent' }
    let(:project) { subject.execute }

    before do
      expect(project).not_to be_saved
    end

    it 'does not set import set import type' do
      expect(project.import_type).to be nil
    end

    it 'does not set import set import source' do
      expect(project.import_source).to be nil
    end

    it 'is not scheduled' do
      expect(project.import_scheduled?).to be(false)
    end

    it 'repository is empty' do
      expect(project.repository.empty?).to be(true)
    end
  end

  context 'the result project' do
    before do
      perform_enqueued_jobs do
        @project = subject.execute
      end

      @project.reload
    end

    it 'overrides template description' do
      expect(@project.description).to match('project description')
    end

    it 'overrides template visibility_level' do
      expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class DownloadService < BaseService
    ALLOWLIST = [
      /^[^.]+\.fogbugz.com$/
    ].freeze

    def initialize(project, url)
      @project = project
      @url = url
    end

    def execute
      return unless valid_url?(@url)

      uploader = FileUploader.new(@project)
      uploader.download!(@url)
      uploader.store!

      uploader.to_h
    end

    private

    def valid_url?(url)
      url && http?(url) && valid_domain?(url)
    end

    def http?(url)
      url =~ /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/
    end

    def valid_domain?(url)
      host = URI.parse(url).host
      ALLOWLIST.any? { |entry| entry === host }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::DownloadService, feature_category: :groups_and_projects do
  describe 'File service' do
    before do
      @user = create(:user)
      @project = create(:project, creator_id: @user.id, namespace: @user.namespace)
    end

    context 'for a URL that is not on allowlist' do
      before do
        url = 'https://code.jquery.com/jquery-2.1.4.min.js'
        @link_to_file = download_file(@project, url)
      end

      it { expect(@link_to_file).to eq(nil) }
    end

    context 'for URLs that are on the allowlist' do
      before do
        # `ssrf_filter` resolves the hostname. See https://github.com/carrierwaveuploader/carrierwave/commit/91714adda998bc9e8decf5b1f5d260d808761304
        stub_request(:get, %r{http://[\d.]+/rails_sample.jpg}).to_return(body: File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'))
        stub_request(:get, %r{http://[\d.]+/doc_sample.txt}).to_return(body: File.read(Rails.root + 'spec/fixtures/doc_sample.txt'))
      end

      context 'an image file' do
        before do
          url = 'http://mycompany.fogbugz.com/rails_sample.jpg'
          @link_to_file = download_file(@project, url)
        end

        it { expect(@link_to_file).to have_key(:alt) }
        it { expect(@link_to_file).to have_key(:url) }
        it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
        it { expect(@link_to_file[:alt]).to eq('rails_sample') }
      end

      context 'a txt file' do
        before do
          url = 'http://mycompany.fogbugz.com/doc_sample.txt'
          @link_to_file = download_file(@project, url)
        end

        it { expect(@link_to_file).to have_key(:alt) }
        it { expect(@link_to_file).to have_key(:url) }
        it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
        it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') }
      end
    end
  end

  def download_file(repository, url)
    Projects::DownloadService.new(repository, url).execute
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class RepositoryLanguagesService < BaseService
    def execute
      perform_language_detection unless project.detected_repository_languages?
      persisted_repository_languages
    end

    private

    def perform_language_detection
      if persisted_repository_languages.blank?
        ::DetectRepositoryLanguagesWorker.perform_async(project.id)
      else
        project.update_column(:detected_repository_languages, true)
      end
    end

    def persisted_repository_languages
      project.repository_languages
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::RepositoryLanguagesService, feature_category: :source_code_management do
  let(:service) { described_class.new(project, project.first_owner) }

  context 'when detected_repository_languages flag is set' do
    let(:project) { create(:project) }

    context 'when a project is without detected programming languages' do
      it 'schedules a worker and returns the empty result' do
        expect(::DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id)
        expect(service.execute).to eq([])
      end
    end

    context 'when a project is with detected programming languages' do
      let!(:repository_language) { create(:repository_language, project: project) }

      it 'does not schedule a worker and returns the detected languages' do
        expect(::DetectRepositoryLanguagesWorker).not_to receive(:perform_async).with(project.id)

        languages = service.execute

        expect(languages.size).to eq(1)
        expect(languages.last.attributes.values).to eq(
          [project.id, repository_language.programming_language_id, repository_language.share]
        )
      end

      it 'sets detected_repository_languages flag' do
        expect { service.execute }.to change(project, :detected_repository_languages).from(nil).to(true)
      end
    end
  end

  context 'when detected_repository_languages flag is not set' do
    let!(:repository_language) { create(:repository_language, project: project) }
    let(:project) { create(:project, detected_repository_languages: true) }
    let(:languages) { service.execute }

    it 'returns repository languages' do
      expect(languages.size).to eq(1)
      expect(languages.last.attributes.values).to eq(
        [project.id, repository_language.programming_language_id, repository_language.share]
      )
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

# Projects::TransferService class
#
# Used to transfer a project to another namespace
#
# Ex.
#   # Move project to namespace by user
#   Projects::TransferService.new(project, user).execute(namespace)
#
module Projects
  class TransferService < BaseService
    include Gitlab::ShellAdapter
    TransferError = Class.new(StandardError)

    def log_project_transfer_success(project, new_namespace)
      log_transfer(project, new_namespace, nil)
    end

    def log_project_transfer_error(project, new_namespace, error_message)
      log_transfer(project, new_namespace, error_message)
    end

    def execute(new_namespace)
      @new_namespace = new_namespace

      if @new_namespace.blank?
        raise TransferError, s_('TransferProject|Please select a new namespace for your project.')
      end

      if @new_namespace.id == project.namespace_id
        raise TransferError, s_('TransferProject|Project is already in this namespace.')
      end

      unless allowed_transfer_project?(current_user, project)
        raise TransferError, s_("TransferProject|You don't have permission to transfer this project.")
      end

      unless allowed_to_transfer_to_namespace?(current_user, @new_namespace)
        raise TransferError, s_("TransferProject|You don't have permission to transfer projects into that namespace.")
      end

      @owner_of_personal_project_before_transfer = project.namespace.owner if project.personal?

      transfer(project)

      log_project_transfer_success(project, @new_namespace)

      true
    rescue Projects::TransferService::TransferError => ex
      project.reset
      project.errors.add(:new_namespace, ex.message)

      log_project_transfer_error(project, @new_namespace, ex.message)

      false
    end

    private

    attr_reader :old_path, :new_path, :new_namespace, :old_namespace

    def log_transfer(project, new_namespace, error_message = nil)
      action = error_message.nil? ? "was" : "was not"

      log_payload = {
        message: "Project #{action} transferred to a new namespace",
        project_id: project.id,
        project_path: project.full_path,
        project_namespace: project.namespace.full_path,
        namespace_id: project.namespace_id,
        new_namespace_id: new_namespace&.id,
        new_project_namespace: new_namespace&.full_path,
        error_message: error_message
      }

      if error_message.nil?
        ::Gitlab::AppLogger.info(log_payload)
      else
        ::Gitlab::AppLogger.error(log_payload)
      end
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def transfer(project)
      @old_path = project.full_path
      @old_group = project.group
      @new_path = File.join(@new_namespace.try(:full_path) || '', project.path)
      @old_namespace = project.namespace

      if Project.where(namespace_id: @new_namespace.try(:id)).where('path = ? or name = ?', project.path, project.name).exists?
        raise TransferError, s_("TransferProject|Project with same name or path in target namespace already exists")
      end

      if project.has_container_registry_tags?
        # We currently don't support renaming repository if it contains tags in container registry
        raise TransferError, s_('TransferProject|Project cannot be transferred, because tags are present in its container registry')
      end

      if !new_namespace_has_same_root?(project) && project.has_namespaced_npm_packages?
        raise TransferError, s_("TransferProject|Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
      end

      proceed_to_transfer
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def new_namespace_has_same_root?(project)
      new_namespace.root_ancestor == project.namespace.root_ancestor
    end

    def proceed_to_transfer
      Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
        %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424282'
      ) do
        Project.transaction do
          project.expire_caches_before_rename(@old_path)

          # Apply changes to the project
          update_namespace_and_visibility(@new_namespace)
          project.reconcile_shared_runners_setting!
          project.save!

          # Notifications
          project.send_move_instructions(@old_path)

          transfer_missing_group_resources(@old_group)

          # Move uploads
          move_project_uploads(project)

          update_integrations

          remove_paid_features

          project.old_path_with_namespace = @old_path

          update_repository_configuration(@new_path)

          remove_issue_contacts

          execute_system_hooks
        end
      end

      update_pending_builds

      post_update_hooks(project, @old_group)
    rescue Exception # rubocop:disable Lint/RescueException
      rollback_side_effects
      raise
    ensure
      refresh_permissions
    end

    # Overridden in EE
    def post_update_hooks(project, _old_group)
      ensure_personal_project_owner_membership(project)
      invalidate_personal_projects_counts

      publish_event
    end

    # Overridden in EE
    def remove_paid_features
    end

    def invalidate_personal_projects_counts
      # If the project was moved out of a personal namespace,
      # the cache of the namespace owner, before the transfer, should be cleared.
      if @owner_of_personal_project_before_transfer.present?
        @owner_of_personal_project_before_transfer.invalidate_personal_projects_count
      end

      # If the project has now moved into a personal namespace,
      # the cache of the target namespace owner should be cleared.
      project.invalidate_personal_projects_count_of_owner
    end

    def transfer_missing_group_resources(group)
      Labels::TransferService.new(current_user, group, project).execute

      Milestones::TransferService.new(current_user, group, project).execute
    end

    def allowed_transfer_project?(current_user, project)
      current_user.can?(:change_namespace, project)
    end

    def allowed_to_transfer_to_namespace?(current_user, namespace)
      current_user.can?(:transfer_projects, namespace)
    end

    def update_namespace_and_visibility(to_namespace)
      # Apply new namespace id and visibility level
      project.namespace = to_namespace
      project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
    end

    def update_repository_configuration(full_path)
      project.set_full_path(gl_full_path: full_path)
      project.track_project_repository
    end

    def ensure_personal_project_owner_membership(project)
      # In case of personal projects, we want to make sure that
      # a membership record with `OWNER` access level exists for the owner of the namespace.
      return unless project.personal?

      namespace_owner = project.namespace.owner
      existing_membership_record = project.member(namespace_owner)

      return if existing_membership_record.present? && existing_membership_record.access_level == Gitlab::Access::OWNER

      project.add_owner(namespace_owner)
    end

    def refresh_permissions
      # This ensures we only schedule 1 job for every user that has access to
      # the namespaces.
      user_ids = @old_namespace.user_ids_for_project_authorizations |
        @new_namespace.user_ids_for_project_authorizations

      AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)

      # Until we compare the inconsistency rates of the new specialized worker and
      # the old approach, we still run AuthorizedProjectsWorker
      # but with some delay and lower urgency as a safety net.
      UserProjectAccessChangedService.new(user_ids).execute(
        priority: UserProjectAccessChangedService::LOW_PRIORITY
      )
    end

    def rollback_side_effects
      project.reset
      update_namespace_and_visibility(@old_namespace)
      update_repository_configuration(@old_path)
    end

    def execute_system_hooks
      system_hook_service.execute_hooks_for(project, :transfer)
    end

    def move_project_uploads(project)
      return if project.hashed_storage?(:attachments)

      Gitlab::UploadsTransfer.new.move_project(
        project.path,
        @old_namespace.full_path,
        @new_namespace.full_path
      )
    end

    def old_wiki_repo_path
      "#{old_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
    end

    def new_wiki_repo_path
      "#{new_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
    end

    def old_design_repo_path
      "#{old_path}#{::Gitlab::GlRepository::DESIGN.path_suffix}"
    end

    def new_design_repo_path
      "#{new_path}#{::Gitlab::GlRepository::DESIGN.path_suffix}"
    end

    def update_integrations
      project.integrations.with_default_settings.delete_all
      Integration.create_from_active_default_integrations(project, :project_id)
    end

    def update_pending_builds
      ::Ci::PendingBuilds::UpdateProjectWorker.perform_async(project.id, pending_builds_params)
    end

    def pending_builds_params
      {
        namespace_id: new_namespace.id,
        namespace_traversal_ids: new_namespace.traversal_ids
      }
    end

    def remove_issue_contacts
      return unless @old_group&.root_ancestor != @new_namespace&.root_ancestor

      CustomerRelations::IssueContact.delete_for_project(project.id)
    end

    def publish_event
      event = ::Projects::ProjectTransferedEvent.new(data: {
        project_id: project.id,
        old_namespace_id: old_namespace.id,
        old_root_namespace_id: old_namespace.root_ancestor.id,
        new_namespace_id: new_namespace.id,
        new_root_namespace_id: new_namespace.root_ancestor.id
      })

      Gitlab::EventStore.publish(event)
    end
  end
end

Projects::TransferService.prepend_mod_with('Projects::TransferService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::TransferService, feature_category: :groups_and_projects do
  let_it_be(:group) { create(:group) }
  let_it_be(:user) { create(:user) }
  let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') }

  let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
  let(:target) { group }
  let(:executor) { user }

  subject(:execute_transfer) { described_class.new(project, executor).execute(target).tap { project.reload } }

  context 'with npm packages' do
    before do
      group.add_owner(user)
    end

    subject(:transfer_service) { described_class.new(project, user) }

    let!(:package) { create(:npm_package, project: project, name: "@testscope/test") }

    context 'with a root namespace change' do
      it 'allow the transfer' do
        expect(transfer_service.execute(group)).to be true
        expect(project.errors[:new_namespace]).to be_empty
      end
    end

    context 'with pending destruction package' do
      before do
        package.pending_destruction!
      end

      it 'allow the transfer' do
        expect(transfer_service.execute(group)).to be true
        expect(project.errors[:new_namespace]).to be_empty
      end
    end

    context 'with namespaced packages present' do
      let!(:package) { create(:npm_package, project: project, name: "@#{project.root_namespace.path}/test") }

      it 'does not allow the transfer' do
        expect(transfer_service.execute(group)).to be false
        expect(project.errors[:new_namespace]).to include("Root namespace can't be updated if the project has NPM packages scoped to the current root level namespace.")
      end
    end

    context 'without a root namespace change' do
      let(:root) { create(:group) }
      let(:group) { create(:group, parent: root) }
      let(:other_group) { create(:group, parent: root) }
      let(:project) { create(:project, :repository, namespace: group) }

      before do
        other_group.add_owner(user)
      end

      it 'allow the transfer' do
        expect(transfer_service.execute(other_group)).to be true
        expect(project.errors[:new_namespace]).to be_empty
      end
    end
  end

  context 'namespace -> namespace' do
    before do
      allow_next_instance_of(Gitlab::UploadsTransfer) do |service|
        allow(service).to receive(:move_project).and_return(true)
      end

      group.add_owner(user)
    end

    it 'updates the namespace' do
      transfer_result = execute_transfer

      expect(transfer_result).to be_truthy
      expect(project.namespace).to eq(group)
    end

    context 'EventStore' do
      let(:group) do
        create(:group, :nested).tap { |g| g.add_owner(user) }
      end

      let(:target) do
        create(:group, :nested).tap { |g| g.add_owner(user) }
      end

      let(:project) { create(:project, namespace: group) }

      it 'publishes a ProjectTransferedEvent' do
        expect { execute_transfer }
          .to publish_event(Projects::ProjectTransferedEvent)
          .with(
            project_id: project.id,
            old_namespace_id: group.id,
            old_root_namespace_id: group.root_ancestor.id,
            new_namespace_id: target.id,
            new_root_namespace_id: target.root_ancestor.id
          )
      end
    end

    context 'when project has an associated project namespace' do
      it 'keeps project namespace in sync with project' do
        transfer_result = execute_transfer

        expect(transfer_result).to be_truthy

        project_namespace_in_sync(group)
      end

      context 'when project is transferred to a deeper nested group' do
        let(:parent_group) { create(:group) }
        let(:sub_group) { create(:group, parent: parent_group) }
        let(:sub_sub_group) { create(:group, parent: sub_group) }
        let(:group) { sub_sub_group }

        it 'keeps project namespace in sync with project' do
          transfer_result = execute_transfer

          expect(transfer_result).to be_truthy

          project_namespace_in_sync(sub_sub_group)
        end
      end
    end
  end

  context 'project in a group -> a personal namespace', :enable_admin_mode do
    let(:project) { create(:project, :repository, :legacy_storage, group: group) }
    let(:target) { user.namespace }
    # We need to use an admin user as the executor because
    # only an admin user has required permissions to transfer projects
    # under _all_ the different circumstances specified below.
    let(:executor) { create(:user, :admin) }

    it 'executes the transfer to personal namespace successfully' do
      execute_transfer

      expect(project.namespace).to eq(user.namespace)
    end

    it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
      expect(user).to receive(:invalidate_personal_projects_count)

      execute_transfer
    end

    context 'the owner of the namespace does not have a direct membership in the project residing in the group' do
      it 'creates a project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
        execute_transfer

        expect(project.members.owners.find_by(user_id: user.id)).to be_present
      end
    end

    context 'the owner of the namespace has a direct membership in the project residing in the group' do
      context 'that membership has an access level of OWNER' do
        before do
          project.add_owner(user)
        end

        it 'retains the project membership record for the owner of the namespace, with OWNER access level, after the transfer' do
          execute_transfer

          expect(project.members.owners.find_by(user_id: user.id)).to be_present
        end
      end

      context 'that membership has an access level that is not OWNER' do
        before do
          project.add_developer(user)
        end

        it 'updates the project membership record for the owner of the namespace, to OWNER access level, after the transfer' do
          execute_transfer

          expect(project.members.owners.find_by(user_id: user.id)).to be_present
        end
      end
    end
  end

  context 'personal namespace -> group', :enable_admin_mode do
    let(:executor) { create(:admin) }

    it 'invalidates personal_project_count cache of the the owner of the personal namespace' \
       'that previously held the project' do
      expect(user).to receive(:invalidate_personal_projects_count)

      execute_transfer
    end
  end

  context 'when transfer succeeds' do
    before do
      group.add_owner(user)
    end

    it 'sends notifications' do
      expect_any_instance_of(NotificationService).to receive(:project_was_moved)

      execute_transfer
    end

    it 'invalidates the user\'s personal_project_count cache' do
      expect(user).to receive(:invalidate_personal_projects_count)

      execute_transfer
    end

    it 'executes system hooks' do
      expect_next_instance_of(described_class) do |service|
        expect(service).to receive(:execute_system_hooks)
      end

      execute_transfer
    end

    it 'moves the disk path', :aggregate_failures do
      old_path = project.repository.disk_path
      old_full_path = project.repository.full_path

      execute_transfer

      project.reload_repository!

      expect(project.repository.disk_path).not_to eq(old_path)
      expect(project.repository.full_path).not_to eq(old_full_path)
      expect(project.disk_path).not_to eq(old_path)
      expect(project.disk_path).to start_with(group.path)
    end

    it 'updates project full path in gitaly' do
      execute_transfer

      expect(project.repository.full_path).to eq "#{group.full_path}/#{project.path}"
    end

    it 'updates storage location' do
      execute_transfer

      expect(project.project_repository).to have_attributes(
        disk_path: "#{group.full_path}/#{project.path}",
        shard_name: project.repository_storage
      )
    end

    context 'with a project integration' do
      let_it_be_with_reload(:project) { create(:project, namespace: user.namespace) }
      let_it_be(:instance_integration) { create(:integrations_slack, :instance) }
      let_it_be(:project_integration) { create(:integrations_slack, project: project) }

      context 'when it inherits from instance_integration' do
        before do
          project_integration.update!(inherit_from_id: instance_integration.id, webhook: instance_integration.webhook)
        end

        it 'replaces inherited integrations', :aggregate_failures do
          expect { execute_transfer }
            .to change(Integration, :count).by(0)
            .and change { project.slack_integration.webhook }.to eq(group_integration.webhook)
        end
      end

      context 'with a custom integration' do
        it 'does not update the integrations' do
          expect { execute_transfer }.not_to change { project.slack_integration.webhook }
        end
      end
    end

    context 'when project has pending builds', :sidekiq_inline do
      let!(:other_project) { create(:project) }
      let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
      let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }

      before do
        group.reload
      end

      it 'updates pending builds for the project', :aggregate_failures do
        execute_transfer

        pending_build.reload
        unrelated_pending_build.reload

        expect(pending_build.namespace_id).to eq(group.id)
        expect(pending_build.namespace_traversal_ids).to eq(group.traversal_ids)
        expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
        expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
      end
    end
  end

  context 'when transfer fails' do
    let!(:original_path) { project.repository.relative_path }

    def attempt_project_transfer(&block)
      expect do
        execute_transfer
      end.to raise_error(ActiveRecord::ActiveRecordError)
    end

    before do
      group.add_owner(user)

      expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR")
    end

    it 'rolls back repo location' do
      attempt_project_transfer

      expect(project.repository.raw.exists?).to be(true)
      expect(original_path).to eq project.repository.relative_path
    end

    it 'rolls back project full path in gitaly' do
      attempt_project_transfer

      expect(project.repository.full_path).to eq project.full_path
    end

    it "doesn't send move notifications" do
      expect_any_instance_of(NotificationService).not_to receive(:project_was_moved)

      attempt_project_transfer
    end

    it "doesn't run system hooks" do
      attempt_project_transfer do |service|
        expect(service).not_to receive(:execute_system_hooks)
      end
    end

    it 'does not update storage location' do
      attempt_project_transfer

      expect(project.project_repository).to have_attributes(
        disk_path: project.disk_path,
        shard_name: project.repository_storage
      )
    end

    it 'does not publish a ProjectTransferedEvent' do
      expect { attempt_project_transfer }
        .not_to publish_event(Projects::ProjectTransferedEvent)
    end

    context 'when project has pending builds', :sidekiq_inline do
      let!(:other_project) { create(:project) }
      let!(:pending_build) { create(:ci_pending_build, project: project.reload) }
      let!(:unrelated_pending_build) { create(:ci_pending_build, project: other_project) }

      it 'does not update pending builds for the project', :aggregate_failures do
        attempt_project_transfer

        pending_build.reload
        unrelated_pending_build.reload

        expect(pending_build.namespace_id).to eq(project.namespace_id)
        expect(pending_build.namespace_traversal_ids).to eq(project.namespace.traversal_ids)
        expect(unrelated_pending_build.namespace_id).to eq(other_project.namespace_id)
        expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
      end
    end

    context 'when project has an associated project namespace' do
      it 'keeps project namespace in sync with project' do
        attempt_project_transfer

        project_namespace_in_sync(user.namespace)
      end
    end
  end

  context 'namespace -> no namespace' do
    let(:group) { nil }

    it 'does not allow the project transfer' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(user.namespace)
      expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.'
    end

    context 'when project has an associated project namespace' do
      it 'keeps project namespace in sync with project' do
        transfer_result = execute_transfer

        expect(transfer_result).to be false

        project_namespace_in_sync(user.namespace)
      end
    end
  end

  context 'disallow transferring of project with tags' do
    let(:container_repository) { create(:container_repository) }

    before do
      stub_container_registry_config(enabled: true)
      stub_container_registry_tags(repository: :any, tags: ['tag'])
      project.container_repositories << container_repository
    end

    it 'does not allow the project transfer' do
      expect(execute_transfer).to eq false
    end
  end

  context 'namespace -> not allowed namespace' do
    it 'does not allow the project transfer' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(user.namespace)
    end
  end

  context 'target namespace containing the same project name' do
    before do
      group.add_owner(user)
      create(:project, name: project.name, group: group, path: 'other')
    end

    it 'does not allow the project transfer' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(user.namespace)
      expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists')
    end
  end

  context 'target namespace containing the same project path' do
    before do
      group.add_owner(user)
      create(:project, name: 'other-name', path: project.path, group: group)
    end

    it 'does not allow the project transfer' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(user.namespace)
      expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists')
    end
  end

  context 'target namespace matches current namespace' do
    let(:group) { user.namespace }

    it 'does not allow project transfer' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(user.namespace)
      expect(project.errors[:new_namespace]).to include('Project is already in this namespace.')
    end
  end

  context 'target namespace belongs to bot user', :enable_admin_mode do
    let(:bot) { create(:user, :project_bot) }
    let(:target) { bot.namespace }
    let(:executor) { create(:user, :admin) }

    it 'does not allow project transfer' do
      namespace = project.namespace

      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.namespace).to eq(namespace)
      expect(project.errors[:new_namespace]).to include("You don't have permission to transfer projects into that namespace.")
    end
  end

  context 'when user does not own the project' do
    let(:project) { create(:project, :repository, :legacy_storage) }

    before do
      project.add_developer(user)
    end

    it 'does not allow project transfer to the target namespace' do
      transfer_result = execute_transfer

      expect(transfer_result).to eq false
      expect(project.errors[:new_namespace]).to include("You don't have permission to transfer this project.")
    end
  end

  context 'when user can create projects in the target namespace' do
    let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }

    context 'but has only developer permissions in the target namespace' do
      before do
        group.add_developer(user)
      end

      it 'does not allow project transfer to the target namespace' do
        transfer_result = execute_transfer

        expect(transfer_result).to eq false
        expect(project.namespace).to eq(user.namespace)
        expect(project.errors[:new_namespace]).to include("You don't have permission to transfer projects into that namespace.")
      end
    end
  end

  context 'visibility level' do
    let(:group) { create(:group, :internal) }

    before do
      group.add_owner(user)
    end

    context 'when namespace visibility level < project visibility level' do
      let(:project) { create(:project, :public, :repository, namespace: user.namespace) }

      before do
        execute_transfer
      end

      it { expect(project.visibility_level).to eq(group.visibility_level) }
    end

    context 'when namespace visibility level > project visibility level' do
      let(:project) { create(:project, :private, :repository, namespace: user.namespace) }

      before do
        execute_transfer
      end

      it { expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
    end
  end

  context 'shared Runners group level configurations' do
    using RSpec::Parameterized::TableSyntax

    where(:project_shared_runners_enabled, :shared_runners_setting, :expected_shared_runners_enabled) do
      true  | :shared_runners_disabled_and_unoverridable | false
      false | :shared_runners_disabled_and_unoverridable | false
      true  | :shared_runners_disabled_and_overridable   | true
      false | :shared_runners_disabled_and_overridable   | false
      true  | :shared_runners_enabled                    | true
      false | :shared_runners_enabled                    | false
    end

    with_them do
      let(:project) { create(:project, :public, :repository, namespace: user.namespace, shared_runners_enabled: project_shared_runners_enabled) }
      let(:group) { create(:group, shared_runners_setting) }

      it 'updates shared runners based on the parent group' do
        group.add_owner(user)

        expect(execute_transfer).to eq(true)

        expect(project.shared_runners_enabled).to eq(expected_shared_runners_enabled)
      end
    end
  end

  context 'missing group labels applied to issues or merge requests' do
    it 'delegates transfer to Labels::TransferService' do
      group.add_owner(user)

      expect_next_instance_of(Labels::TransferService, user, project.group, project) do |labels_transfer_service|
        expect(labels_transfer_service).to receive(:execute).once.and_call_original
      end

      execute_transfer
    end
  end

  context 'missing group milestones applied to issues or merge requests' do
    it 'delegates transfer to Milestones::TransferService' do
      group.add_owner(user)

      expect_next_instance_of(Milestones::TransferService, user, project.group, project) do |milestones_transfer_service|
        expect(milestones_transfer_service).to receive(:execute).once.and_call_original
      end

      execute_transfer
    end
  end

  context 'when hashed storage in use' do
    let!(:project) { create(:project, :repository, namespace: user.namespace) }
    let!(:old_disk_path) { project.repository.disk_path }

    before do
      group.add_owner(user)
    end

    it 'does not move the disk path', :aggregate_failures do
      new_full_path = "#{group.full_path}/#{project.path}"

      execute_transfer

      project.reload_repository!

      expect(project.repository).to have_attributes(
        disk_path: old_disk_path,
        full_path: new_full_path
      )
      expect(project.disk_path).to eq(old_disk_path)
    end

    it 'does not move the disk path when the transfer fails', :aggregate_failures do
      old_full_path = project.full_path

      expect_next_instance_of(described_class) do |service|
        allow(service).to receive(:execute_system_hooks).and_raise('foo')
      end

      expect { execute_transfer }.to raise_error('foo')

      project.reload_repository!

      expect(project.repository).to have_attributes(
        disk_path: old_disk_path,
        full_path: old_full_path
      )
      expect(project.disk_path).to eq(old_disk_path)
    end
  end

  describe 'refreshing project authorizations' do
    let(:old_group) { create(:group) }
    let!(:project) { create(:project, namespace: old_group) }
    let(:member_of_old_group) { create(:user) }
    let(:group) { create(:group) }
    let(:member_of_new_group) { create(:user) }

    before do
      old_group.add_developer(member_of_old_group)
      group.add_maintainer(member_of_new_group)

      # Add the executing user as owner in both groups, so that
      # transfer can be executed.
      old_group.add_owner(user)
      group.add_owner(user)
    end

    it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
      expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
        .to receive(:perform_async).with(project.id)

      execute_transfer
    end

    it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
      stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)

      user_ids = [user.id, member_of_old_group.id, member_of_new_group.id].map { |id| [id] }

      expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
        receive(:bulk_perform_in).with(
          1.hour,
          user_ids,
          batch_delay: 30.seconds, batch_size: 100
        )
      )

      subject
    end

    it 'refreshes the permissions of the members of the old and new namespace', :sidekiq_inline do
      expect { execute_transfer }
        .to change { member_of_old_group.authorized_projects.include?(project) }.from(true).to(false)
        .and change { member_of_new_group.authorized_projects.include?(project) }.from(false).to(true)
    end
  end

  describe 'transferring a design repository' do
    subject { described_class.new(project, user) }

    before do
      group.add_owner(user)
    end

    def design_repository
      project.design_repository
    end

    def clear_design_repo_memoization
      project&.design_management_repository&.clear_memoization(:repository)
      project.clear_memoization(:design_repository)
    end

    it 'does not create a design repository' do
      expect(subject.execute(group)).to be true

      clear_design_repo_memoization

      expect(design_repository.exists?).to be false
    end

    describe 'when the project has a design repository' do
      let(:project_repo_path) { "#{project.path}#{::Gitlab::GlRepository::DESIGN.path_suffix}" }
      let(:old_full_path) { "#{user.namespace.full_path}/#{project_repo_path}" }
      let(:new_full_path) { "#{group.full_path}/#{project_repo_path}" }

      context 'with legacy storage' do
        let(:project) { create(:project, :repository, :legacy_storage, :design_repo, namespace: user.namespace) }

        it 'moves the repository' do
          expect(subject.execute(group)).to be true

          clear_design_repo_memoization

          expect(design_repository).to have_attributes(
            disk_path: new_full_path,
            full_path: new_full_path
          )
        end

        it 'does not move the repository when an error occurs', :aggregate_failures do
          allow(subject).to receive(:execute_system_hooks).and_raise('foo')
          expect { subject.execute(group) }.to raise_error('foo')

          clear_design_repo_memoization

          expect(design_repository).to have_attributes(
            disk_path: old_full_path,
            full_path: old_full_path
          )
        end
      end

      context 'with hashed storage' do
        let(:project) { create(:project, :repository, namespace: user.namespace) }

        it 'does not move the repository' do
          old_disk_path = design_repository.disk_path

          expect(subject.execute(group)).to be true

          clear_design_repo_memoization

          expect(design_repository).to have_attributes(
            disk_path: old_disk_path,
            full_path: new_full_path
          )
        end

        it 'does not move the repository when an error occurs' do
          old_disk_path = design_repository.disk_path

          allow(subject).to receive(:execute_system_hooks).and_raise('foo')
          expect { subject.execute(group) }.to raise_error('foo')

          clear_design_repo_memoization

          expect(design_repository).to have_attributes(
            disk_path: old_disk_path,
            full_path: old_full_path
          )
        end
      end
    end
  end

  context 'handling issue contacts' do
    let_it_be(:root_group) { create(:group) }

    let(:project) { create(:project, group: root_group) }

    before do
      root_group.add_owner(user)
      target.add_owner(user)
      create_list(:issue_customer_relations_contact, 2, :for_issue, issue: create(:issue, project: project))
    end

    context 'with the same root_ancestor' do
      let(:target) { create(:group, parent: root_group) }

      it 'retains issue contacts' do
        expect { execute_transfer }.not_to change { CustomerRelations::IssueContact.count }
      end
    end

    context 'with a different root_ancestor' do
      it 'deletes issue contacts' do
        expect { execute_transfer }.to change { CustomerRelations::IssueContact.count }.by(-2)
      end
    end
  end

  def project_namespace_in_sync(group)
    project.reload
    expect(project.namespace).to eq(group)
    expect(project.project_namespace.visibility_level).to eq(project.visibility_level)
    expect(project.project_namespace.path).to eq(project.path)
    expect(project.project_namespace.parent).to eq(project.namespace)
    expect(project.project_namespace.traversal_ids).to eq([*project.namespace.traversal_ids, project.project_namespace.id])
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class RefreshBuildArtifactsSizeStatisticsService
    BATCH_SIZE = 500
    REFRESH_INTERVAL_SECONDS = 0.1

    def execute
      refresh = Projects::BuildArtifactsSizeRefresh.process_next_refresh!

      return unless refresh&.running?

      batch = refresh.next_batch(limit: BATCH_SIZE).to_a

      if batch.any?
        increments = batch.map do |artifact|
          Gitlab::Counters::Increment.new(amount: artifact.size.to_i, ref: artifact.id)
        end

        Projects::BuildArtifactsSizeRefresh.transaction do
          # Mark the refresh ready for another worker to pick up and process the next batch
          refresh.requeue!(batch.last.id)

          ProjectStatistics.bulk_increment_statistic(refresh.project, :build_artifacts_size, increments)
        end

        sleep REFRESH_INTERVAL_SECONDS
      else
        refresh.schedule_finalize!
      end

      refresh
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitlab_redis_shared_state, feature_category: :build_artifacts do
  let(:service) { described_class.new }

  describe '#execute' do
    let_it_be(:project, reload: true) { create(:project) }

    let_it_be(:artifact_1) { create(:ci_job_artifact, project: project, size: 1, created_at: 14.days.ago) }
    let_it_be(:artifact_2) { create(:ci_job_artifact, project: project, size: 2, created_at: 13.days.ago) }
    let_it_be(:artifact_3) { create(:ci_job_artifact, project: project, size: nil, created_at: 13.days.ago) }
    let_it_be(:artifact_4) { create(:ci_job_artifact, project: project, size: 5, created_at: 12.days.ago) }

    # This should not be included in the recalculation as it is created later than the refresh start time
    let_it_be(:future_artifact) { create(:ci_job_artifact, project: project, size: 8, created_at: 2.days.from_now) }

    let!(:refresh) do
      create(
        :project_build_artifacts_size_refresh,
        :created,
        project: project,
        updated_at: 2.days.ago,
        refresh_started_at: nil,
        last_job_artifact_id: nil
      )
    end

    let(:now) { Time.zone.now }
    let(:statistics) { project.statistics }
    let(:increment) { Gitlab::Counters::Increment.new(amount: 30) }

    around do |example|
      freeze_time { example.run }
    end

    before do
      stub_const("#{described_class}::BATCH_SIZE", 3)
      stub_const("#{described_class}::REFRESH_INTERVAL_SECONDS", 0)

      stats = create(:project_statistics, project: project, build_artifacts_size: 120)
      stats.increment_counter(:build_artifacts_size, increment)
    end

    it 'resets the build artifacts size stats' do
      expect { service.execute }.to change { statistics.reload.build_artifacts_size }.from(120).to(0)
    end

    it 'resets the buffered counter' do
      expect { service.execute }
        .to change { Gitlab::Counters::BufferedCounter.new(statistics, :build_artifacts_size).get }.to(0)
    end

    it 'updates the last_job_artifact_id to the ID of the last artifact from the batch' do
      expect { service.execute }.to change { refresh.reload.last_job_artifact_id.to_i }.to(artifact_3.id)
    end

    it 'updates the last_job_artifact_id to the ID of the last artifact from the project' do
      expect { service.execute }
        .to change { refresh.reload.last_job_artifact_id_on_refresh_start.to_i }
              .to(project.job_artifacts.last.id)
    end

    it 'requeues the refresh job' do
      service.execute
      expect(refresh.reload).to be_pending
    end

    context 'when an error happens after the recalculation has started' do
      let!(:refresh) do
        create(
          :project_build_artifacts_size_refresh,
          :pending,
          project: project,
          last_job_artifact_id: artifact_3.id,
          last_job_artifact_id_on_refresh_start: artifact_4.id
        )
      end

      before do
        allow(Gitlab::Redis::SharedState).to receive(:with).and_raise(StandardError, 'error')

        expect { service.execute }.to raise_error(StandardError)
      end

      it 'keeps the last_job_artifact_id unchanged' do
        expect(refresh.reload.last_job_artifact_id).to eq(artifact_3.id)
      end

      it 'keeps the last_job_artifact_id_on_refresh_start unchanged' do
        expect(refresh.reload.last_job_artifact_id_on_refresh_start).to eq(artifact_4.id)
      end

      it 'keeps the state of the refresh record at running' do
        expect(refresh.reload).to be_running
      end
    end

    context 'when there are no more artifacts to recalculate for the next refresh job' do
      let!(:refresh) do
        create(
          :project_build_artifacts_size_refresh,
          :pending,
          project: project,
          updated_at: 2.days.ago,
          refresh_started_at: now,
          last_job_artifact_id: artifact_4.id
        )
      end

      it 'schedules the refresh to be finalized' do
        service.execute

        expect(refresh.reload.finalizing?).to be(true)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true

module Projects
  class ForkService < BaseService
    def execute(fork_to_project = nil)
      forked_project = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project

      if forked_project&.saved?
        refresh_forks_count
        stream_audit_event(forked_project)
      end

      forked_project
    end

    def valid_fork_targets(options = {})
      @valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute(options)
    end

    def valid_fork_branch?(branch)
      @project.repository.branch_exists?(branch)
    end

    def valid_fork_target?(namespace = target_namespace)
      return true if current_user.admin?

      valid_fork_targets.include?(namespace)
    end

    private

    def link_existing_project(fork_to_project)
      return if fork_to_project.forked?

      build_fork_network_member(fork_to_project)

      fork_to_project if link_fork_network(fork_to_project)
    end

    def fork_new_project
      new_project = CreateService.new(current_user, new_fork_params).execute
      return new_project unless new_project.persisted?

      new_project.project_feature.update!(
        @project.project_feature.slice(ProjectFeature::FEATURES.map { |f| "#{f}_access_level" })
      )

      new_project
    end

    def new_fork_params
      new_params = {
        forked_from_project: @project,
        visibility_level: target_visibility_level,
        description: target_description,
        name: target_name,
        path: target_path,
        shared_runners_enabled: @project.shared_runners_enabled,
        namespace_id: target_namespace.id,
        fork_network: fork_network,
        ci_config_path: @project.ci_config_path,
        # We need to set ci_default_git_depth to 0 for the forked project when
        # @project.ci_default_git_depth is nil in order to keep the same behaviour
        # and not get ProjectCiCdSetting::DEFAULT_GIT_DEPTH set on create
        ci_cd_settings_attributes: { default_git_depth: @project.ci_default_git_depth || 0 },
        # We need to assign the fork network membership after the project has
        # been instantiated to avoid ActiveRecord trying to create it when
        # initializing the project, as that would cause a foreign key constraint
        # exception.
        relations_block: -> (project) { build_fork_network_member(project) },
        skip_disk_validation: skip_disk_validation,
        external_authorization_classification_label: @project.external_authorization_classification_label,
        suggestion_commit_message: @project.suggestion_commit_message,
        merge_commit_template: @project.merge_commit_template,
        squash_commit_template: @project.squash_commit_template,
        import_data: { data: { fork_branch: branch } }
      }

      if @project.avatar.present? && @project.avatar.image?
        new_params[:avatar] = @project.avatar
      end

      new_params[:mr_default_target_self] = target_mr_default_target_self unless target_mr_default_target_self.nil?

      new_params.merge!(@project.object_pool_params)

      new_params
    end

    def allowed_fork?
      current_user.can?(:fork_project, @project)
    end

    def fork_network
      @fork_network ||= @project.fork_network || @project.build_root_of_fork_network
    end

    def build_fork_network_member(fork_to_project)
      if allowed_fork?
        fork_to_project.build_fork_network_member(
          forked_from_project: @project,
          fork_network: fork_network
        )
      else
        fork_to_project.errors.add(:forked_from_project_id, 'is forbidden')
      end
    end

    def link_fork_network(fork_to_project)
      return if fork_to_project.errors.any?

      fork_to_project.fork_network_member.save
    end

    def refresh_forks_count
      Projects::ForksCountService.new(@project).refresh_cache
    end

    def target_path
      @target_path ||= @params[:path] || @project.path
    end

    def target_name
      @target_name ||= @params[:name] || @project.name
    end

    def target_description
      @target_description ||= @params[:description] || @project.description
    end

    def target_namespace
      @target_namespace ||= @params[:namespace] || current_user.namespace
    end

    def skip_disk_validation
      @skip_disk_validation ||= @params[:skip_disk_validation] || false
    end

    def target_visibility_level
      target_level = [@project.visibility_level, target_namespace.visibility_level].min
      target_level = [target_level, Gitlab::VisibilityLevel.level_value(params[:visibility])].min if params.key?(:visibility)

      Gitlab::VisibilityLevel.closest_allowed_level(target_level)
    end

    def target_mr_default_target_self
      @target_mr_default_target_self ||= params[:mr_default_target_self]
    end

    def stream_audit_event(forked_project)
      # Defined in EE
    end

    def branch
      # We extract branch name from @params[:branches] because the front end
      # insists on sending it as 'branches'.
      @params[:branches]
    end
  end
end

Projects::ForkService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ForkService, feature_category: :source_code_management do
  include ProjectForksHelper

  shared_examples 'forks count cache refresh' do
    it 'flushes the forks count cache of the source project', :clean_gitlab_redis_cache do
      expect(from_project.forks_count).to be_zero

      fork_project(from_project, to_user, using_service: true)
      BatchLoader::Executor.clear_current

      expect(from_project.forks_count).to eq(1)
    end
  end

  context 'when forking a new project' do
    describe 'fork by user' do
      before do
        @from_user = create(:user)
        @from_namespace = @from_user.namespace
        avatar = fixture_file_upload("spec/fixtures/dk.png", "image/png")
        @from_project = create(
          :project,
          :repository,
          creator_id: @from_user.id,
          namespace: @from_namespace,
          star_count: 107,
          avatar: avatar,
          description: 'wow such project',
          external_authorization_classification_label: 'classification-label'
        )
        @to_user = create(:user)
        @to_namespace = @to_user.namespace
        @from_project.add_member(@to_user, :developer)
      end

      context 'fork project' do
        context 'when forker is a guest' do
          before do
            @guest = create(:user)
            @from_project.add_member(@guest, :guest)
          end
          subject { fork_project(@from_project, @guest, using_service: true) }

          it { is_expected.not_to be_persisted }
          it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }

          it 'does not create a fork network' do
            expect { subject }.not_to change { @from_project.reload.fork_network }
          end
        end

        it_behaves_like 'forks count cache refresh' do
          let(:from_project) { @from_project }
          let(:to_user) { @to_user }
        end

        describe "successfully creates project in the user namespace" do
          let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace, using_service: true) }

          it { expect(to_project).to be_persisted }
          it { expect(to_project.errors).to be_empty }
          it { expect(to_project.first_owner).to eq(@to_user) }
          it { expect(to_project.namespace).to eq(@to_user.namespace) }
          it { expect(to_project.star_count).to be_zero }
          it { expect(to_project.description).to eq(@from_project.description) }
          it { expect(to_project.avatar.file).to be_exists }
          it { expect(to_project.ci_config_path).to eq(@from_project.ci_config_path) }
          it { expect(to_project.external_authorization_classification_label).to eq(@from_project.external_authorization_classification_label) }
          it { expect(to_project.suggestion_commit_message).to eq(@from_project.suggestion_commit_message) }
          it { expect(to_project.merge_commit_template).to eq(@from_project.merge_commit_template) }
          it { expect(to_project.squash_commit_template).to eq(@from_project.squash_commit_template) }

          # This test is here because we had a bug where the from-project lost its
          # avatar after being forked.
          # https://gitlab.com/gitlab-org/gitlab-foss/issues/26158
          it "after forking the from-project still has its avatar" do
            # If we do not fork the project first we cannot detect the bug.
            expect(to_project).to be_persisted

            expect(@from_project.avatar.file).to be_exists
          end

          it_behaves_like 'forks count cache refresh' do
            let(:from_project) { @from_project }
            let(:to_user) { @to_user }
          end

          it 'creates a fork network with the new project and the root project set' do
            to_project
            fork_network = @from_project.reload.fork_network

            expect(fork_network).not_to be_nil
            expect(fork_network.root_project).to eq(@from_project)
            expect(fork_network.projects).to contain_exactly(@from_project, to_project)
          end

          it 'imports the repository of the forked project', :sidekiq_might_not_need_inline do
            to_project = fork_project(@from_project, @to_user, repository: true, using_service: true)

            expect(to_project.empty_repo?).to be_falsy
          end
        end

        context 'creating a fork of a fork' do
          let(:from_forked_project) { fork_project(@from_project, @to_user, using_service: true) }
          let(:other_namespace) do
            group = create(:group)
            group.add_owner(@to_user)
            group
          end

          let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace, using_service: true) }

          it 'sets the root of the network to the root project' do
            expect(to_project.fork_network.root_project).to eq(@from_project)
          end

          it 'sets the forked_from_project on the membership' do
            expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
          end

          context 'when the forked project has higher visibility than the root project' do
            let(:root_project) { create(:project, :public) }

            it 'successfully creates a fork of the fork with correct visibility' do
              forked_project = fork_project(root_project, @to_user, using_service: true)

              root_project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)

              # Forked project visibility is not affected by root project visibility change
              expect(forked_project).to have_attributes(visibility_level: Gitlab::VisibilityLevel::PUBLIC)

              fork_of_the_fork = fork_project(forked_project, @to_user, namespace: other_namespace, using_service: true)

              expect(fork_of_the_fork).to be_valid
              expect(fork_of_the_fork).to have_attributes(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
            end
          end

          it_behaves_like 'forks count cache refresh' do
            let(:from_project) { from_forked_project }
            let(:to_user) { @to_user }
          end
        end
      end

      context 'project already exists' do
        it "fails due to validation, not transaction failure" do
          @existing_project = create(:project, :repository, creator_id: @to_user.id, path: @from_project.path, namespace: @to_namespace)
          @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace, using_service: true)
          expect(@existing_project).to be_persisted

          expect(@to_project).not_to be_persisted
          expect(@to_project.errors[:path]).to eq(['has already been taken'])
        end
      end

      context 'repository in legacy storage already exists' do
        let(:raw_fake_repo) { Gitlab::Git::Repository.new('default', File.join(@to_user.namespace.full_path, "#{@from_project.path}.git"), nil, nil) }
        let(:params) { { namespace: @to_user.namespace, using_service: true } }

        before do
          stub_application_setting(hashed_storage_enabled: false)
          raw_fake_repo.create_repository
        end

        after do
          raw_fake_repo.remove
        end

        subject { fork_project(@from_project, @to_user, params) }

        it 'does not allow creation' do
          expect(subject).not_to be_persisted
          expect(subject.errors.messages).to have_key(:base)
          expect(subject.errors.messages[:base].first).to match('There is already a repository with that name on disk')
        end

        context 'when repository disk validation is explicitly skipped' do
          let(:params) { super().merge(skip_disk_validation: true) }

          it 'allows fork project creation' do
            expect(subject).to be_persisted
            expect(subject.errors.messages).to be_empty
          end
        end
      end

      context "CI/CD settings" do
        let(:to_project) { fork_project(@from_project, @to_user, using_service: true) }

        context "when origin has git depth specified" do
          before do
            @from_project.update!(ci_default_git_depth: 42)
          end

          it "inherits default_git_depth from the origin project" do
            expect(to_project.ci_default_git_depth).to eq(42)
          end
        end

        context "when origin does not define git depth" do
          before do
            @from_project.update!(ci_default_git_depth: nil)
          end

          it "the fork has git depth set to 0" do
            expect(to_project.ci_default_git_depth).to eq(0)
          end
        end
      end

      context "when project has restricted visibility level" do
        context "and only one visibility level is restricted" do
          before do
            @from_project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
            stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
          end

          it "creates fork with lowest level" do
            forked_project = fork_project(@from_project, @to_user, using_service: true)

            expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
          end
        end

        context "and all visibility levels are restricted" do
          before do
            stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
          end

          it "creates fork with private visibility levels" do
            forked_project = fork_project(@from_project, @to_user, using_service: true)

            expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
          end
        end
      end

      context 'when forking is disabled' do
        before do
          @from_project.project_feature.update_attribute(
            :forking_access_level, ProjectFeature::DISABLED)
        end

        it 'fails' do
          to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace, using_service: true)

          expect(to_project.errors[:forked_from_project_id]).to eq(['is forbidden'])
        end
      end
    end

    describe 'fork to namespace' do
      before do
        @group_owner = create(:user)
        @developer   = create(:user)
        @project     = create(
          :project, :repository,
          creator_id: @group_owner.id,
          star_count: 777,
          description: 'Wow, such a cool project!',
          ci_config_path: 'debian/salsa-ci.yml'
        )
        @group = create(:group)
        @group.add_member(@group_owner, GroupMember::OWNER)
        @group.add_member(@developer,   GroupMember::DEVELOPER)
        @project.add_member(@developer,   :developer)
        @project.add_member(@group_owner, :developer)
        @opts = { namespace: @group, using_service: true }
      end

      context 'fork project for group' do
        it 'group owner successfully forks project into the group' do
          to_project = fork_project(@project, @group_owner, @opts)

          expect(to_project).to                be_persisted
          expect(to_project.errors).to         be_empty
          expect(to_project.first_owner).to    eq(@group_owner)
          expect(to_project.namespace).to      eq(@group)
          expect(to_project.name).to           eq(@project.name)
          expect(to_project.path).to           eq(@project.path)
          expect(to_project.description).to    eq(@project.description)
          expect(to_project.ci_config_path).to eq(@project.ci_config_path)
          expect(to_project.star_count).to     be_zero
        end
      end

      context 'fork project for group when user not owner' do
        it 'group developer fails to fork project into the group' do
          to_project = fork_project(@project, @developer, @opts)

          expect(to_project.errors[:namespace]).to eq(['is not valid'])
        end
      end

      context 'project already exists in group' do
        it 'fails due to validation, not transaction failure' do
          existing_project = create(:project, :repository, path: @project.path, namespace: @group)
          to_project = fork_project(@project, @group_owner, @opts)
          expect(existing_project.persisted?).to be_truthy
          expect(to_project.errors[:path]).to eq(['has already been taken'])
        end
      end

      context 'when the namespace has a lower visibility level than the project' do
        it 'creates the project with the lower visibility level' do
          public_project = create(:project, :public)
          private_group = create(:group, :private)
          group_owner = create(:user)
          private_group.add_owner(group_owner)

          forked_project = fork_project(public_project, group_owner, namespace: private_group, using_service: true)

          expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
        end
      end
    end

    describe 'fork with optional attributes' do
      let(:public_project) { create(:project, :public) }

      it 'sets optional attributes to specified values' do
        forked_project = fork_project(
          public_project,
          nil,
          namespace: public_project.namespace,
          path: 'forked',
          name: 'My Fork',
          description: 'Description',
          visibility: 'internal',
          using_service: true
        )

        expect(forked_project.path).to eq('forked')
        expect(forked_project.name).to eq('My Fork')
        expect(forked_project.description).to eq('Description')
        expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
      end

      it 'sets visibility level to private if an unknown visibility is requested' do
        forked_project = fork_project(public_project, nil, using_service: true, visibility: 'unknown')

        expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
      end

      it 'sets visibility level to project visibility level if requested visibility is greater' do
        private_project = create(:project, :private)

        forked_project = fork_project(private_project, nil, using_service: true, visibility: 'public')

        expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
      end

      it 'sets visibility level to target namespace visibility level if requested visibility is greater' do
        private_group = create(:group, :private)

        forked_project = fork_project(public_project, nil, namespace: private_group, using_service: true, visibility: 'public')

        expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
      end

      it 'copies project features visibility settings to the fork', :aggregate_failures do
        attrs = ProjectFeature::FEATURES.to_h do |f|
          ["#{f}_access_level", ProjectFeature::PRIVATE]
        end

        public_project.project_feature.update!(attrs)

        user = create(:user, developer_projects: [public_project])
        forked_project = described_class.new(public_project, user).execute

        expect(forked_project.project_feature.slice(attrs.keys)).to eq(attrs)
      end
    end
  end

  context 'when a project is already forked' do
    it 'creates a new pool repository after the project is moved to a new shard' do
      project = create(:project, :public, :repository)
      fork_before_move = fork_project(project, nil, using_service: true)

      # Stub everything required to move a project to a Gitaly shard that does not exist
      allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original
      allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid)
      stub_storage_settings('test_second_storage' => {})
      allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_repository)
        .and_return(true)
      allow_any_instance_of(Gitlab::Git::Repository).to receive(:replicate)
      allow_any_instance_of(Gitlab::Git::Repository).to receive(:checksum)
        .and_return(::Gitlab::Git::BLANK_SHA)
      allow_next_instance_of(Gitlab::Git::ObjectPool) do |object_pool|
        allow(object_pool).to receive(:link)
      end

      storage_move = create(
        :project_repository_storage_move,
        :scheduled,
        container: project,
        destination_storage_name: 'test_second_storage'
      )
      Projects::UpdateRepositoryStorageService.new(storage_move).execute
      fork_after_move = fork_project(project.reload, nil, using_service: true)
      pool_repository_before_move = PoolRepository.joins(:shard)
                                      .find_by(source_project: project, shards: { name: 'default' })
      pool_repository_after_move = PoolRepository.joins(:shard)
                                     .find_by(source_project: project, shards: { name: 'test_second_storage' })

      expect(fork_before_move.pool_repository).to eq(pool_repository_before_move)
      expect(fork_after_move.pool_repository).to eq(pool_repository_after_move)
    end
  end

  context 'when forking with object pools' do
    let(:fork_from_project) { create(:project, :repository, :public) }
    let(:forker) { create(:user) }

    context 'when no pool exists' do
      it 'creates a new object pool' do
        forked_project = fork_project(fork_from_project, forker, using_service: true)

        expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
      end
    end

    context 'when a pool already exists' do
      let!(:pool_repository) { create(:pool_repository, source_project: fork_from_project) }

      it 'joins the object pool' do
        forked_project = fork_project(fork_from_project, forker, using_service: true)

        expect(forked_project.pool_repository).to eq(fork_from_project.pool_repository)
      end
    end
  end

  context 'when linking fork to an existing project' do
    let(:fork_from_project) { create(:project, :public) }
    let(:fork_to_project) { create(:project, :public) }
    let(:user) do
      create(:user).tap { |u| fork_to_project.add_maintainer(u) }
    end

    subject { described_class.new(fork_from_project, user) }

    def forked_from_project(project)
      project.fork_network_member&.forked_from_project
    end

    context 'if project is already forked' do
      it 'does not create fork relation' do
        allow(fork_to_project).to receive(:forked?).and_return(true)
        expect(forked_from_project(fork_to_project)).to be_nil
        expect(subject.execute(fork_to_project)).to be_nil
        expect(forked_from_project(fork_to_project)).to be_nil
      end
    end

    context 'if project is not forked' do
      it 'creates fork relation' do
        expect(fork_to_project.forked?).to be_falsy
        expect(forked_from_project(fork_to_project)).to be_nil

        subject.execute(fork_to_project)

        fork_to_project.reload

        expect(fork_to_project.forked?).to be true
        expect(forked_from_project(fork_to_project)).to eq fork_from_project
        expect(fork_to_project.forked_from_project).to eq fork_from_project
      end

      it 'flushes the forks count cache of the source project' do
        expect(fork_from_project.forks_count).to be_zero

        subject.execute(fork_to_project)
        BatchLoader::Executor.clear_current

        expect(fork_from_project.forks_count).to eq(1)
      end

      context 'if the fork is not allowed' do
        let(:fork_from_project) { create(:project, :private) }

        it 'does not delete the LFS objects' do
          create(:lfs_objects_project, project: fork_to_project)

          expect { subject.execute(fork_to_project) }
            .not_to change { fork_to_project.lfs_objects_projects.size }
        end
      end
    end
  end

  describe '#valid_fork_targets' do
    let(:finder_mock) { instance_double('ForkTargetsFinder', execute: ['finder_return_value']) }
    let(:current_user) { instance_double('User') }
    let(:project) { instance_double('Project') }

    before do
      allow(ForkTargetsFinder).to receive(:new).with(project, current_user).and_return(finder_mock)
    end

    it 'returns whatever finder returns' do
      expect(described_class.new(project, current_user).valid_fork_targets).to eq ['finder_return_value']
    end
  end

  describe '#valid_fork_branch?' do
    let_it_be(:user) { create(:user) }
    let_it_be(:project) { create(:project, :small_repo, creator_id: user.id) }
    let_it_be(:branch) { nil }

    subject { described_class.new(project, user).valid_fork_branch?(branch) }

    context 'when branch exists' do
      let(:branch) { project.default_branch_or_main }

      it { is_expected.to be_truthy }
    end

    context 'when branch does not exist' do
      let(:branch) { 'branch-that-does-not-exist' }

      it { is_expected.to be_falsey }
    end
  end

  describe '#valid_fork_target?' do
    let(:project) { Project.new }
    let(:params) { {} }

    context 'when target is not passed' do
      subject { described_class.new(project, user, params).valid_fork_target? }

      context 'when current user is an admin' do
        let(:user) { build(:user, :admin) }

        it { is_expected.to be_truthy }
      end

      context 'when current_user is not an admin' do
        let(:user) { create(:user) }

        let(:finder_mock) { instance_double('ForkTargetsFinder', execute: [user.namespace]) }
        let(:project) { create(:project) }

        before do
          allow(ForkTargetsFinder).to receive(:new).with(project, user).and_return(finder_mock)
        end

        context 'when target namespace is in valid fork targets' do
          let(:params) { { namespace: user.namespace } }

          it { is_expected.to be_truthy }
        end

        context 'when target namespace is not in valid fork targets' do
          let(:params) { { namespace: create(:group) } }

          it { is_expected.to be_falsey }
        end
      end
    end

    context 'when target is passed' do
      let(:target) { create(:group) }

      subject { described_class.new(project, user, params).valid_fork_target?(target) }

      context 'when current user is an admin' do
        let(:user) { build(:user, :admin) }

        it { is_expected.to be_truthy }
      end

      context 'when current user is not an admin' do
        let(:user) { create(:user) }

        before do
          allow(ForkTargetsFinder).to receive(:new).with(project, user).and_return(finder_mock)
        end

        context 'when target namespace is in valid fork targets' do
          let(:finder_mock) { instance_double('ForkTargetsFinder', execute: [target]) }

          it { is_expected.to be_truthy }
        end

        context 'when target namespace is not in valid fork targets' do
          let(:finder_mock) { instance_double('ForkTargetsFinder', execute: [create(:group)]) }

          it { is_expected.to be_falsey }
        end
      end
    end
  end
end