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

module Projects
  class UnlinkForkService < BaseService
    # Close existing MRs coming from the project and remove it from the fork network
    def execute(refresh_statistics: true)
      fork_network = @project.fork_network
      forked_from = @project.forked_from_project

      return unless fork_network

      log_info(message: "UnlinkForkService: Unlinking fork network", fork_network_id: fork_network.id)

      merge_requests = fork_network
                         .merge_requests
                         .opened
                         .from_and_to_forks(@project)

      merge_requests.find_each do |mr|
        ::MergeRequests::CloseService.new(project: @project, current_user: @current_user).execute(mr)
        log_info(message: "UnlinkForkService: Closed merge request", merge_request_id: mr.id)
      end

      Project.transaction do
        # Get out of the fork network as a member and
        # remove references from all its direct forks.
        @project.fork_network_member.destroy
        @project.forked_to_members.update_all(forked_from_project_id: nil)

        # The project is not necessarily a fork, so update the fork network originating
        # from this project
        if fork_network = @project.root_of_fork_network
          fork_network.update(root_project: nil, deleted_root_project_name: @project.full_name)
        end

        @project.leave_pool_repository
      end

      # rubocop: disable Cop/InBatches
      Project.uncached do
        @project.forked_to_members.in_batches do |fork_relation|
          fork_relation.pluck(:id).each do |fork_id| # rubocop: disable CodeReuse/ActiveRecord
            log_info(message: "UnlinkForkService: Unlinked fork of root_project", project_id: @project.id, forked_project_id: fork_id)
          end
        end
      end
      # rubocop: enable Cop/InBatches

      if Feature.enabled?(:refresh_statistics_on_unlink_fork, @project.namespace) && refresh_statistics
        ProjectCacheWorker.perform_async(project.id, [], [:repository_size])
      end

      # When the project getting out of the network is a node with parent
      # and children, both the parent and the node needs a cache refresh.
      [forked_from, @project].compact.each do |project|
        refresh_forks_count(project)
      end
    end

    private

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

Projects::UnlinkForkService.prepend_mod_with('Projects::UnlinkForkService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_caching, feature_category: :source_code_management do
  include ProjectForksHelper

  subject { described_class.new(forked_project, user) }

  let(:project) { create(:project, :public) }
  let!(:forked_project) { fork_project(project, user) }
  let(:user) { create(:user) }

  context 'with opened merge request on the source project' do
    let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: forked_project.forked_from_project) }
    let(:merge_request2) { create(:merge_request, source_project: forked_project, target_project: fork_project(project)) }
    let(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }

    let(:mr_close_service) { MergeRequests::CloseService.new(project: forked_project, current_user: user) }

    before do
      allow(MergeRequests::CloseService).to receive(:new)
        .with(project: forked_project, current_user: user)
        .and_return(mr_close_service)
    end

    it 'close all pending merge requests' do
      expect(mr_close_service).to receive(:execute).with(merge_request)
      expect(mr_close_service).to receive(:execute).with(merge_request2)

      subject.execute
    end

    it 'does not close merge requests for the project being unlinked' do
      expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)
    end
  end

  it 'removes the link to the fork network' do
    expect(forked_project.fork_network_member).to be_present
    expect(forked_project.fork_network).to be_present

    subject.execute
    forked_project.reload

    expect(forked_project.fork_network_member).to be_nil
    expect(forked_project.reload.fork_network).to be_nil
  end

  it 'refreshes the forks count cache of the source project' do
    source = forked_project.forked_from_project

    expect(source.forks_count).to eq(1)

    subject.execute
    BatchLoader::Executor.clear_current

    expect(source.forks_count).to be_zero
  end

  it 'refreshes the project statistics of the forked project' do
    expect(ProjectCacheWorker).to receive(:perform_async).with(forked_project.id, [], [:repository_size])

    subject.execute
  end

  it 'does not refresh project statistics when refresh_statistics is false' do
    expect(ProjectCacheWorker).not_to receive(:perform_async)

    subject.execute(refresh_statistics: false)
  end

  it 'does not refresh project statistics when the feature flag is disabled' do
    stub_feature_flags(refresh_statistics_on_unlink_fork: false)

    expect(ProjectCacheWorker).not_to receive(:perform_async)

    subject.execute
  end

  context 'when the original project was deleted' do
    it 'does not fail when the original project is deleted' do
      source = forked_project.forked_from_project
      source.destroy!
      forked_project.reload

      expect { subject.execute }.not_to raise_error
    end
  end

  context 'when given project is a source of forks' do
    let!(:forked_project_2) { fork_project(project, user) }
    let!(:fork_of_fork) { fork_project(forked_project, user) }

    subject { described_class.new(project, user) }

    context 'with opened merge requests from fork back to root project' do
      let!(:merge_request) { create(:merge_request, source_project: project, target_project: forked_project) }
      let!(:merge_request2) { create(:merge_request, source_project: project, target_project: fork_project(project)) }
      let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }

      let(:mr_close_service) { MergeRequests::CloseService.new(project: project, current_user: user) }

      before do
        allow(MergeRequests::CloseService).to receive(:new)
                                                .with(project: project, current_user: user)
                                                .and_return(mr_close_service)
      end

      it 'closes all pending merge requests' do
        expect(mr_close_service).to receive(:execute).with(merge_request)
        expect(mr_close_service).to receive(:execute).with(merge_request2)

        subject.execute
      end

      it 'does not close merge requests that do not come from the project being unlinked' do
        expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)

        subject.execute
      end
    end

    it 'removes its link to the fork network and updates direct network members' do
      expect(project.fork_network_member).to be_present
      expect(project.fork_network).to be_present
      expect(project.forked_to_members.count).to eq(2)
      expect(forked_project.forked_to_members.count).to eq(1)
      expect(fork_of_fork.forked_to_members.count).to eq(0)

      subject.execute

      project.reload
      forked_project.reload
      fork_of_fork.reload

      expect(project.fork_network_member).to be_nil
      expect(project.fork_network).to be_nil
      expect(forked_project.fork_network).to have_attributes(
        root_project_id: nil,
        deleted_root_project_name: project.full_name
      )
      expect(project.forked_to_members.count).to eq(0)
      expect(forked_project.forked_to_members.count).to eq(1)
      expect(fork_of_fork.forked_to_members.count).to eq(0)
    end

    it 'refreshes the forks count cache of the given project' do
      expect(project.forks_count).to eq(2)

      subject.execute
      BatchLoader::Executor.clear_current

      expect(project.forks_count).to be_zero
    end

    context 'and is node with a parent' do
      subject { described_class.new(forked_project, user) }

      context 'with opened merge requests from and to given project' do
        let!(:mr_from_parent) { create(:merge_request, source_project: project, target_project: forked_project) }
        let!(:mr_to_parent) { create(:merge_request, source_project: forked_project, target_project: project) }
        let!(:mr_to_child) { create(:merge_request, source_project: forked_project, target_project: fork_of_fork) }
        let!(:mr_from_child) { create(:merge_request, source_project: fork_of_fork, target_project: forked_project) }
        let!(:merge_request_in_fork) { create(:merge_request, source_project: forked_project, target_project: forked_project) }

        let(:mr_close_service) { MergeRequests::CloseService.new(project: forked_project, current_user: user) }

        before do
          allow(MergeRequests::CloseService).to receive(:new)
            .with(project: forked_project, current_user: user)
            .and_return(mr_close_service)
        end

        it 'close all pending merge requests' do
          merge_requests = [mr_from_parent, mr_to_parent, mr_from_child, mr_to_child]

          merge_requests.each do |mr|
            expect(mr_close_service).to receive(:execute).with(mr).and_call_original
          end

          subject.execute

          merge_requests = MergeRequest.where(id: merge_requests)

          expect(merge_requests).to all(have_attributes(state: 'closed'))
        end

        it 'does not close merge requests which do not come from the project being unlinked' do
          expect(mr_close_service).not_to receive(:execute).with(merge_request_in_fork)

          subject.execute
        end
      end

      it 'refreshes the forks count cache of the parent and the given project' do
        expect(project.forks_count).to eq(2)
        expect(forked_project.forks_count).to eq(1)

        subject.execute
        BatchLoader::Executor.clear_current

        expect(project.forks_count).to eq(1)
        expect(forked_project.forks_count).to eq(0)
      end

      it 'removes its link to the fork network and updates direct network members' do
        expect(project.fork_network).to be_present
        expect(forked_project.fork_network).to be_present
        expect(fork_of_fork.fork_network).to be_present

        expect(project.forked_to_members.count).to eq(2)
        expect(forked_project.forked_to_members.count).to eq(1)
        expect(fork_of_fork.forked_to_members.count).to eq(0)

        subject.execute
        project.reload
        forked_project.reload
        fork_of_fork.reload

        expect(project.fork_network).to be_present
        expect(forked_project.fork_network).to be_nil
        expect(fork_of_fork.fork_network).to be_present

        expect(project.forked_to_members.count).to eq(1) # 1 child is gone
        expect(forked_project.forked_to_members.count).to eq(0)
        expect(fork_of_fork.forked_to_members.count).to eq(0)
      end
    end
  end

  context 'a project with pool repository' do
    let(:project) { create(:project, :public, :repository) }
    let!(:pool_repository) { create(:pool_repository, :ready, source_project: project) }

    subject { described_class.new(project, user) }

    it 'when unlinked leaves pool repository' do
      expect { subject.execute }.to change { project.reload.has_pool_repository? }.from(true).to(false)
    end
  end

  context 'when given project is not part of a fork network' do
    let!(:project_without_forks) { create(:project, :public) }

    subject { described_class.new(project_without_forks, user) }

    it 'does not raise errors' do
      expect { subject.execute }.not_to raise_error
    end
  end
end


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

module Projects
  # Service class for getting and caching the number of forks of a project.
  class ForksCountService < Projects::CountService
    def cache_key_name
      'forks_count'
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def self.query(project_ids)
      ForkNetworkMember.where(forked_from_project: project_ids)
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ForksCountService, :use_clean_rails_memory_store_caching, feature_category: :source_code_management do
  let(:project) { build(:project) }

  subject { described_class.new(project) }

  it_behaves_like 'a counter caching service'

  describe '#count' do
    it 'returns the number of forks' do
      allow(subject).to receive(:uncached_count).and_return(1)

      expect(subject.count).to eq(1)
    end
  end
end


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

module Projects
  # The CleanupService removes data from the project repository following a
  # BFG rewrite: https://rtyley.github.io/bfg-repo-cleaner/
  #
  # Before executing this service, all refs rewritten by BFG should have been
  # pushed to the repository
  class CleanupService < BaseService
    NoUploadError = StandardError.new("Couldn't find uploaded object map")

    include Gitlab::Utils::StrongMemoize

    class << self
      def enqueue(project, current_user, bfg_object_map)
        Projects::UpdateService.new(project, current_user, bfg_object_map: bfg_object_map).execute.tap do |result|
          next unless result[:status] == :success

          project.set_repository_read_only!
          RepositoryCleanupWorker.perform_async(project.id, current_user.id)
        end
      rescue Project::RepositoryReadOnlyError => err
        { status: :error, message: (_('Failed to make repository read-only. %{reason}') % { reason: err.message }) }
      end

      def cleanup_after(project)
        project.bfg_object_map.remove!
        project.set_repository_writable!
      end
    end

    # Attempt to clean up the project following the push. Warning: this is
    # destructive!
    #
    # path is the path of an upload of a BFG object map file. It contains a line
    # per rewritten object, with the old and new SHAs space-separated. It can be
    # used to update or remove content that references the objects that BFG has
    # altered
    def execute
      apply_bfg_object_map!

      # Remove older objects that are no longer referenced
      Projects::GitGarbageCollectWorker.new.perform(project.id, :prune, "project_cleanup:gc:#{project.id}")

      # The cache may now be inaccurate, and holding onto it could prevent
      # bugs assuming the presence of some object from manifesting for some
      # time. Better to feel the pain immediately.
      project.repository.expire_all_method_caches

      self.class.cleanup_after(project)
    end

    private

    def apply_bfg_object_map!
      raise NoUploadError unless project.bfg_object_map.exists?

      project.bfg_object_map.open do |io|
        repository_cleaner.apply_bfg_object_map_stream(io) do |response|
          cleanup_diffs(response)
        end
      end
    end

    def cleanup_diffs(response)
      old_commit_shas = extract_old_commit_shas(response.entries)

      ApplicationRecord.transaction do
        cleanup_merge_request_diffs(old_commit_shas)
        cleanup_note_diff_files(old_commit_shas)
      end
    end

    def extract_old_commit_shas(batch)
      batch.lazy.select { |entry| entry.type == :COMMIT }.map(&:old_oid).force
    end

    def cleanup_merge_request_diffs(old_commit_shas)
      merge_request_diffs = MergeRequestDiff
        .by_project_id(project.id)
        .by_commit_sha(old_commit_shas)

      # It's important to run the ActiveRecord callbacks here
      merge_request_diffs.destroy_all # rubocop:disable Cop/DestroyAll

      # TODO: ensure the highlight cache is removed immediately. It's too hard
      # to calculate the Redis keys at present.
      #
      # https://gitlab.com/gitlab-org/gitlab-foss/issues/61115
    end

    def cleanup_note_diff_files(old_commit_shas)
      # Pluck the IDs instead of running the query twice to ensure we clear the
      # cache for exactly the note diffs we remove
      ids = NoteDiffFile
        .referencing_sha(old_commit_shas, project_id: project.id)
        .pluck_primary_key

      NoteDiffFile.id_in(ids).delete_all

      # A highlighted version of the diff is stored in redis. Remove it now.
      Gitlab::DiscussionsDiff::HighlightCache.clear_multiple(ids)
    end

    def repository_cleaner
      @repository_cleaner ||= Gitlab::Git::RepositoryCleaner.new(repository.raw)
    end
  end
end

Projects::CleanupService.prepend_mod_with('Projects::CleanupService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::CleanupService, feature_category: :source_code_management do
  subject(:service) { described_class.new(project) }

  describe '.enqueue' do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:user) { create(:user) }

    let(:object_map_file) { fixture_file_upload('spec/fixtures/bfg_object_map.txt') }

    subject(:enqueue) { described_class.enqueue(project, user, object_map_file) }

    it 'makes the repository read-only' do
      expect { enqueue }
        .to change(project, :repository_read_only?)
        .from(false)
        .to(true)
    end

    it 'sets the bfg_object_map of the project' do
      enqueue

      expect(project.bfg_object_map.read).to eq(object_map_file.read)
    end

    it 'enqueues a RepositoryCleanupWorker' do
      enqueue

      expect(RepositoryCleanupWorker.jobs.count).to eq(1)
    end

    it 'returns success' do
      expect(enqueue[:status]).to eq(:success)
    end

    it 'returns an error if making the repository read-only fails' do
      project.set_repository_read_only!

      expect(enqueue[:status]).to eq(:error)
    end

    it 'returns an error if updating the project fails' do
      expect_next_instance_of(Projects::UpdateService) do |service|
        expect(service).to receive(:execute).and_return(status: :error)
      end

      expect(enqueue[:status]).to eq(:error)
      expect(project.reload.repository_read_only?).to be_falsy
    end
  end

  describe '.cleanup_after' do
    let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) }

    subject(:cleanup_after) { described_class.cleanup_after(project) }

    before do
      project.set_repository_read_only!
    end

    it 'sets the repository read-write' do
      expect { cleanup_after }.to change(project, :repository_read_only?).from(true).to(false)
    end

    it 'removes the BFG object map' do
      cleanup_after

      expect(project.bfg_object_map).not_to be_exist
    end
  end

  describe '#execute' do
    let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) }
    let(:object_map) { project.bfg_object_map }
    let(:cleaner) { service.__send__(:repository_cleaner) }

    before do
      project.set_repository_read_only!
    end

    it 'runs the apply_bfg_object_map_stream gitaly RPC' do
      expect(cleaner).to receive(:apply_bfg_object_map_stream).with(kind_of(IO))

      service.execute
    end

    it 'runs garbage collection on the repository' do
      expect_next_instance_of(Projects::GitGarbageCollectWorker) do |worker|
        expect(worker).to receive(:perform).with(project.id, :prune, "project_cleanup:gc:#{project.id}")
      end

      service.execute
    end

    it 'clears the repository cache' do
      expect(project.repository).to receive(:expire_all_method_caches)

      service.execute
    end

    it 'removes the object map file' do
      service.execute

      expect(object_map.exists?).to be_falsy
    end

    it 'makes the repository read-write again' do
      expect { service.execute }
        .to change(project, :repository_read_only?)
        .from(true)
        .to(false)
    end

    context 'with a tainted merge request diff' do
      let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
      let(:diff) { merge_request.merge_request_diff }
      let(:entry) { build_entry(diff.commits.first.id) }

      before do
        allow(cleaner)
          .to receive(:apply_bfg_object_map_stream)
          .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
      end

      it 'removes the tainted commit from the database' do
        service.execute

        expect(MergeRequestDiff.exists?(diff.id)).to be_falsy
      end

      it 'ignores non-commit responses from Gitaly' do
        entry.type = :UNKNOWN

        service.execute

        expect(MergeRequestDiff.exists?(diff.id)).to be_truthy
      end
    end

    context 'with a tainted diff note' do
      let(:diff_note) { create(:diff_note_on_commit, project: project) }
      let(:note_diff_file) { diff_note.note_diff_file }
      let(:entry) { build_entry(diff_note.commit_id) }

      let(:highlight_cache) { Gitlab::DiscussionsDiff::HighlightCache }
      let(:cache_id) { note_diff_file.id }

      before do
        allow(cleaner)
          .to receive(:apply_bfg_object_map_stream)
          .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
      end

      it 'removes the tainted commit from the database' do
        service.execute

        expect(NoteDiffFile.exists?(note_diff_file.id)).to be_falsy
      end

      it 'removes the highlight cache from redis' do
        write_cache(highlight_cache, cache_id, [{}])

        expect(read_cache(highlight_cache, cache_id)).not_to be_nil

        service.execute

        expect(read_cache(highlight_cache, cache_id)).to be_nil
      end

      it 'ignores non-commit responses from Gitaly' do
        entry.type = :UNKNOWN

        service.execute

        expect(NoteDiffFile.exists?(note_diff_file.id)).to be_truthy
      end
    end

    it 'raises an error if no object map can be found' do
      object_map.remove!

      expect { service.execute }.to raise_error(described_class::NoUploadError)
    end
  end

  def build_entry(old_oid)
    Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new(
      type: :COMMIT,
      old_oid: old_oid,
      new_oid: Gitlab::Git::BLANK_SHA
    )
  end

  def read_cache(cache, key)
    cache.read_multiple([key]).first
  end

  def write_cache(cache, key, value)
    cache.write_multiple(key => value)
  end
end


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

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

    MAX_TRIES = 3

    def execute(remote_mirror, tries)
      return success unless remote_mirror.enabled?

      # Blocked URLs are a hard failure, no need to attempt to retry
      if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url), schemes: Project::VALID_MIRROR_PROTOCOLS)
        hard_retry_or_fail(remote_mirror, _('The remote mirror URL is invalid.'), tries)
        return error(remote_mirror.last_error)
      end

      update_mirror(remote_mirror)

      success
    rescue Gitlab::Git::CommandError => e
      # This happens if one of the gitaly calls above fail, for example when
      # branches have diverged, or the pre-receive hook fails.
      hard_retry_or_fail(remote_mirror, e.message, tries)

      error(e.message)
    rescue StandardError => e
      remote_mirror.hard_fail!(e.message)
      raise e
    end

    private

    def normalized_url(url)
      strong_memoize(:normalized_url) do
        CGI.unescape(Gitlab::UrlSanitizer.sanitize(url))
      end
    end

    def update_mirror(remote_mirror)
      remote_mirror.update_start!

      # LFS objects must be sent first, or the push has dangling pointers
      lfs_status = send_lfs_objects!(remote_mirror)

      response = remote_mirror.update_repository
      failed, failure_message = failure_status(lfs_status, response, remote_mirror)

      # When the issue https://gitlab.com/gitlab-org/gitlab/-/issues/349262 is closed,
      # we can move this block within failure_status.
      if failed
        remote_mirror.mark_as_failed!(failure_message)
      else
        remote_mirror.update_finish!
      end
    end

    def failure_status(lfs_status, response, remote_mirror)
      message = ''
      failed = false
      lfs_sync_failed = false

      if lfs_status&.dig(:status) == :error
        lfs_sync_failed = true
        message += "Error synchronizing LFS files:"
        message += "\n\n#{lfs_status[:message]}\n\n"

        failed = Feature.enabled?(:remote_mirror_fail_on_lfs, project)
      end

      if response.divergent_refs.any?
        message += "Some refs have diverged and have not been updated on the remote:"
        message += "\n\n#{response.divergent_refs.join("\n")}"
        failed = true
      end

      if message.present?
        Gitlab::AppJsonLogger.info(
          message: "Error synching remote mirror",
          project_id: project.id,
          project_path: project.full_path,
          remote_mirror_id: remote_mirror.id,
          lfs_sync_failed: lfs_sync_failed,
          divergent_ref_list: response.divergent_refs
        )
      end

      [failed, message]
    end

    def send_lfs_objects!(remote_mirror)
      return unless project.lfs_enabled?

      # TODO: Support LFS sync over SSH
      # https://gitlab.com/gitlab-org/gitlab/-/issues/249587
      return unless %r{\Ahttps?://}i.match?(remote_mirror.url)
      return unless remote_mirror.password_auth?

      Lfs::PushService.new(
        project,
        current_user,
        url: remote_mirror.bare_url,
        credentials: remote_mirror.credentials
      ).execute
    end

    def hard_retry_or_fail(mirror, message, tries)
      if tries < MAX_TRIES
        mirror.hard_retry!(message)
      else
        # It's not likely we'll be able to recover from this ourselves, so we'll
        # notify the users of the problem, and don't trigger any sidekiq retries
        # Instead, we'll wait for the next change to try the push again, or until
        # a user manually retries.
        mirror.hard_fail!(message)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::UpdateRemoteMirrorService, feature_category: :source_code_management do
  let_it_be(:project) { create(:project, :repository, lfs_enabled: true) }
  let_it_be(:remote_project) { create(:forked_project_with_submodules) }
  let_it_be(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }

  subject(:service) { described_class.new(project, project.creator) }

  describe '#execute' do
    let(:retries) { 0 }

    subject(:execute!) { service.execute(remote_mirror, retries) }

    before do
      project.repository.add_branch(project.first_owner, 'existing-branch', 'master')

      allow(remote_mirror)
        .to receive(:update_repository)
        .and_return(double(divergent_refs: []))
    end

    it 'does not fetch the remote repository' do
      # See https://gitlab.com/gitlab-org/gitaly/-/issues/2670
      expect(project.repository).not_to receive(:fetch_remote)

      execute!
    end

    it 'marks the mirror as started when beginning' do
      expect(remote_mirror).to receive(:update_start!).and_call_original

      execute!
    end

    it 'marks the mirror as successfully finished' do
      result = execute!

      expect(result[:status]).to eq(:success)
      expect(remote_mirror).to be_finished
    end

    it 'marks the mirror as failed and raises the error when an unexpected error occurs' do
      allow(remote_mirror).to receive(:update_repository).and_raise('Badly broken')

      expect { execute! }.to raise_error(/Badly broken/)

      expect(remote_mirror).to be_failed
      expect(remote_mirror.last_error).to include('Badly broken')
    end

    context 'when the URL is blocked' do
      before do
        allow(Gitlab::UrlBlocker).to receive(:blocked_url?).and_return(true)
      end

      it 'hard retries and returns error status' do
        expect(execute!).to eq(status: :error, message: 'The remote mirror URL is invalid.')
        expect(remote_mirror).to be_to_retry
      end

      context 'when retries are exceeded' do
        let(:retries) { 4 }

        it 'hard fails and returns error status' do
          expect(execute!).to eq(status: :error, message: 'The remote mirror URL is invalid.')
          expect(remote_mirror).to be_failed
        end
      end
    end

    context "when given URLs containing escaped elements" do
      it_behaves_like "URLs containing escaped elements return expected status" do
        let(:result) { execute! }

        before do
          allow(remote_mirror).to receive(:url).and_return(url)
        end
      end
    end

    context 'when the update fails because of a `Gitlab::Git::CommandError`' do
      before do
        allow(remote_mirror).to receive(:update_repository)
          .and_raise(Gitlab::Git::CommandError.new('update failed'))
      end

      it 'wraps `Gitlab::Git::CommandError`s in a service error' do
        expect(execute!).to eq(status: :error, message: 'update failed')
      end

      it 'marks the mirror as to be retried' do
        execute!

        expect(remote_mirror).to be_to_retry
        expect(remote_mirror.last_error).to include('update failed')
      end

      it "marks the mirror as failed after #{described_class::MAX_TRIES} tries" do
        service.execute(remote_mirror, described_class::MAX_TRIES)

        expect(remote_mirror).to be_failed
        expect(remote_mirror.last_error).to include('update failed')
      end
    end

    context 'when there are divergent refs' do
      it 'marks the mirror as failed and sets an error message' do
        response = double(divergent_refs: %w[refs/heads/master refs/heads/develop])
        expect(remote_mirror).to receive(:update_repository).and_return(response)

        execute!

        expect(remote_mirror).to be_failed
        expect(remote_mirror.last_error).to include("Some refs have diverged")
        expect(remote_mirror.last_error).to include("refs/heads/master\n")
        expect(remote_mirror.last_error).to include("refs/heads/develop")
      end
    end

    context "sending lfs objects" do
      let_it_be(:lfs_pointer) { create(:lfs_objects_project, project: project) }

      before do
        stub_lfs_setting(enabled: true)
      end

      it 'pushes LFS objects to a HTTP repository' do
        expect_next_instance_of(Lfs::PushService) do |service|
          expect(service).to receive(:execute)
        end
        expect(Gitlab::AppJsonLogger).not_to receive(:info)

        execute!

        expect(remote_mirror.update_status).to eq('finished')
        expect(remote_mirror.last_error).to be_nil
      end

      context 'when LFS objects fail to push' do
        before do
          expect_next_instance_of(Lfs::PushService) do |service|
            expect(service).to receive(:execute).and_return({ status: :error, message: 'unauthorized' })
          end
        end

        context 'when remote_mirror_fail_on_lfs feature flag enabled' do
          it 'fails update' do
            expect(Gitlab::AppJsonLogger).to receive(:info).with(
              hash_including(message: "Error synching remote mirror")).and_call_original

            execute!

            expect(remote_mirror.update_status).to eq('failed')
            expect(remote_mirror.last_error).to eq("Error synchronizing LFS files:\n\nunauthorized\n\n")
          end
        end

        context 'when remote_mirror_fail_on_lfs feature flag is disabled' do
          before do
            stub_feature_flags(remote_mirror_fail_on_lfs: false)
          end

          it 'does not fail update' do
            expect(Gitlab::AppJsonLogger).to receive(:info).with(
              hash_including(message: "Error synching remote mirror")).and_call_original

            execute!

            expect(remote_mirror.update_status).to eq('finished')
            expect(remote_mirror.last_error).to be_nil
          end
        end
      end

      context 'with SSH repository' do
        let(:ssh_mirror) { create(:remote_mirror, project: project, enabled: true) }

        before do
          allow(ssh_mirror)
            .to receive(:update_repository)
            .and_return(double(divergent_refs: []))
        end

        it 'does nothing to an SSH repository' do
          ssh_mirror.update!(url: 'ssh://example.com')

          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)

          service.execute(ssh_mirror, retries)
        end

        it 'does nothing if LFS is disabled' do
          expect(project).to receive(:lfs_enabled?) { false }

          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)

          service.execute(ssh_mirror, retries)
        end

        it 'does nothing if non-password auth is specified' do
          ssh_mirror.update!(auth_method: 'ssh_public_key')

          expect_any_instance_of(Lfs::PushService).not_to receive(:execute)

          service.execute(ssh_mirror, retries)
        end
      end
    end
  end
end


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

module Projects
  class MoveAccessService < BaseMoveRelationsService
    def execute(source_project, remove_remaining_elements: true)
      return unless super

      @project.with_transaction_returning_status do
        if @project.namespace != source_project.namespace
          @project.run_after_commit do
            source_project.namespace.refresh_project_authorizations
            self.namespace.refresh_project_authorizations
          end
        end

        ::Projects::MoveProjectMembersService.new(@project, @current_user)
          .execute(source_project, remove_remaining_elements: remove_remaining_elements)
        ::Projects::MoveProjectGroupLinksService.new(@project, @current_user)
          .execute(source_project, remove_remaining_elements: remove_remaining_elements)
        ::Projects::MoveProjectAuthorizationsService.new(@project, @current_user)
          .execute(source_project, remove_remaining_elements: remove_remaining_elements)

        @project.save(touch: false)

        success
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::MoveAccessService, feature_category: :groups_and_projects do
  let(:user) { create(:user) }
  let(:group) { create(:group) }
  let(:project_with_access) { create(:project, namespace: user.namespace) }
  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
    project_with_access.add_maintainer(maintainer_user)
    project_with_access.add_developer(developer_user)
    project_with_access.add_reporter(reporter_user)
    project_with_access.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
    project_with_access.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
    project_with_access.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER)
  end

  subject { described_class.new(target_project, user) }

  describe '#execute' do
    shared_examples 'move the accesses' do
      it 'moves the accesses', :sidekiq_inline do
        expect(project_with_access.project_members.count).to eq 4
        expect(project_with_access.project_group_links.count).to eq 3
        expect(project_with_access.authorized_users.count).to eq 4

        subject.execute(project_with_access)

        expect(project_with_access.project_members.count).to eq 0
        expect(project_with_access.project_group_links.count).to eq 0
        expect(project_with_access.authorized_users.count).to eq 1
        expect(target_project.project_members.count).to eq 4
        expect(target_project.project_group_links.count).to eq 3
        expect(target_project.authorized_users.count).to eq 4
      end

      it 'rollbacks if an exception is raised' do
        allow(subject).to receive(:success).and_raise(StandardError)

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

        expect(project_with_access.project_members.count).to eq 4
        expect(project_with_access.project_group_links.count).to eq 3
        expect(project_with_access.authorized_users.count).to eq 4
      end
    end

    context 'when both projects are in the same namespace' do
      let(:target_project) { create(:project, namespace: user.namespace) }

      it 'does not refresh project owner authorized projects' do
        allow(project_with_access).to receive(:namespace).and_return(user.namespace)
        expect(project_with_access.namespace).not_to receive(:refresh_project_authorizations)
        expect(target_project.namespace).not_to receive(:refresh_project_authorizations)

        subject.execute(project_with_access)
      end

      it_behaves_like 'move the accesses'
    end

    context 'when projects are in different namespaces' do
      let(:target_project) { create(:project, namespace: group) }

      before do
        group.add_owner(user)
      end

      it 'refreshes both project owner authorized projects' do
        allow(project_with_access).to receive(:namespace).and_return(user.namespace)
        expect(user.namespace).to receive(:refresh_project_authorizations).once
        expect(group).to receive(:refresh_project_authorizations).once

        subject.execute(project_with_access)
      end

      it_behaves_like 'move the accesses'
    end

    context 'when remove_remaining_elements is false' do
      let(:target_project) { create(:project, namespace: user.namespace) }
      let(:options) { { remove_remaining_elements: false } }

      it 'does not remove remaining memberships' do
        target_project.add_maintainer(maintainer_user)

        subject.execute(project_with_access, **options)

        expect(project_with_access.project_members.count).not_to eq 0
      end

      it 'does not remove remaining group links' do
        target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)

        subject.execute(project_with_access, **options)

        expect(project_with_access.project_group_links.count).not_to eq 0
      end

      it 'does not remove remaining authorizations' do
        target_project.add_developer(developer_user)

        subject.execute(project_with_access, **options)

        expect(project_with_access.project_authorizations.count).not_to eq 0
      end
    end
  end
end


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

# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if necessary
module Projects
  class MoveProjectAuthorizationsService < BaseMoveRelationsService
    def execute(source_project, remove_remaining_elements: true)
      return unless super

      Project.transaction do
        move_project_authorizations

        remove_remaining_authorizations if remove_remaining_elements

        success
      end
    end

    private

    def move_project_authorizations
      non_existent_authorization.update_all(project_id: @project.id)
    end

    def remove_remaining_authorizations
      # I think because the Project Authorization table does not have a primary key
      # it brings a lot a problems/bugs. First, Rails raises PG::SyntaxException if we use
      # destroy_all instead of delete_all.
      source_project.project_authorizations.delete_all(:delete_all)
    end

    # Look for authorizations in source_project that are not in the target project
    # rubocop: disable CodeReuse/ActiveRecord
    def non_existent_authorization
      source_project.project_authorizations
                    .select(:user_id)
                    .where.not(user: @project.authorized_users)
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::MoveProjectAuthorizationsService, feature_category: :groups_and_projects do
  let!(:user) { create(:user) }
  let(:project_with_users) { create(:project, namespace: user.namespace) }
  let(:target_project) { create(:project, namespace: user.namespace) }
  let(:maintainer_user) { create(:user) }
  let(:reporter_user) { create(:user) }
  let(:developer_user) { create(:user) }

  subject { described_class.new(target_project, user) }

  describe '#execute' do
    before do
      project_with_users.add_maintainer(maintainer_user)
      project_with_users.add_developer(developer_user)
      project_with_users.add_reporter(reporter_user)
    end

    it 'moves the authorizations from one project to another' do
      expect(project_with_users.authorized_users.count).to eq 4
      expect(target_project.authorized_users.count).to eq 1

      subject.execute(project_with_users)

      expect(project_with_users.authorized_users.count).to eq 0
      expect(target_project.authorized_users.count).to eq 4
    end

    it 'does not move existent authorizations to the current project' do
      target_project.add_maintainer(developer_user)
      target_project.add_developer(reporter_user)

      expect(project_with_users.authorized_users.count).to eq 4
      expect(target_project.authorized_users.count).to eq 3

      subject.execute(project_with_users)

      expect(project_with_users.authorized_users.count).to eq 0
      expect(target_project.authorized_users.count).to eq 4
    end

    context 'when remove_remaining_elements is false' do
      let(:options) { { remove_remaining_elements: false } }

      it 'does not remove remaining project authorizations' do
        target_project.add_maintainer(developer_user)
        target_project.add_developer(reporter_user)

        subject.execute(project_with_users, **options)

        expect(project_with_users.project_authorizations.count).not_to eq 0
      end
    end
  end
end


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

module Projects
  # Tries to schedule a move for every project with repositories on the source shard
  class ScheduleBulkRepositoryShardMovesService
    include ScheduleBulkRepositoryShardMovesMethods
    extend ::Gitlab::Utils::Override

    private

    override :repository_klass
    def repository_klass
      ProjectRepository
    end

    override :container_klass
    def container_klass
      Project
    end

    override :container_column
    def container_column
      :project_id
    end

    override :schedule_bulk_worker_klass
    def self.schedule_bulk_worker_klass
      ::Projects::ScheduleBulkRepositoryShardMovesWorker
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService, feature_category: :source_code_management do
  it_behaves_like 'moves repository shard in bulk' do
    let_it_be_with_reload(:container) { create(:project, :repository) }

    let(:move_service_klass) { Projects::RepositoryStorageMove }
    let(:bulk_worker_klass) { ::Projects::ScheduleBulkRepositoryShardMovesWorker }
  end
end


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

module Projects
  class ReadmeRendererService < BaseService
    include Rails.application.routes.url_helpers

    TEMPLATE_PATH = Rails.root.join('app', 'views', 'projects', 'readme_templates')

    def execute
      render(params[:template_name] || :default)
    end

    private

    def render(template_name)
      ERB.new(File.read(sanitized_filename(template_name)), trim_mode: '<>').result(binding)
    end

    def sanitized_filename(template_name)
      path = Gitlab::PathTraversal.check_path_traversal!("#{template_name}.md.tt")
      path = TEMPLATE_PATH.join(path).to_s
      Gitlab::PathTraversal.check_allowed_absolute_path!(path, [TEMPLATE_PATH.to_s])

      path
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ReadmeRendererService, '#execute', feature_category: :groups_and_projects do
  using RSpec::Parameterized::TableSyntax

  subject(:service) { described_class.new(project, nil, opts) }

  let_it_be(:project) { create(:project, title: 'My Project', description: '_custom_description_') }

  let(:opts) { { default_branch: 'master' } }

  it 'renders the an ERB readme template' do
    expect(service.execute).to start_with(<<~MARKDOWN)
      # My Project

      _custom_description_

      ## Getting started

      To make it easy for you to get started with GitLab, here's a list of recommended next steps.

      Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!

      ## Add your files

      - [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
      - [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:

      ```
      cd existing_repo
      git remote add origin #{project.http_url_to_repo}
      git branch -M master
      git push -uf origin master
      ```
    MARKDOWN
  end

  context 'with a custom template' do
    before do
      allow(File).to receive(:read).and_call_original
    end

    it 'renders that template file' do
      opts[:template_name] = :custom_readme

      expect(service).to receive(:sanitized_filename).with(:custom_readme).and_return('custom_readme.md.tt')
      expect(File).to receive(:read).with('custom_readme.md.tt').and_return('_custom_readme_file_content_')
      expect(service.execute).to eq('_custom_readme_file_content_')
    end

    context 'with path traversal in mind' do
      where(:template_name, :exception, :expected_path) do
        '../path/traversal/bad' | [Gitlab::PathTraversal::PathTraversalAttackError, 'Invalid path'] | nil
        '/bad/template' | [StandardError, 'path /bad/template.md.tt is not allowed'] | nil
        'good/template' | nil | 'good/template.md.tt'
      end

      with_them do
        it 'raises the expected exception on bad paths' do
          opts[:template_name] = template_name

          if exception
            expect { subject.execute }.to raise_error(*exception)
          else
            expect(File).to receive(:read).with(described_class::TEMPLATE_PATH.join(expected_path).to_s).and_return('')

            expect { subject.execute }.not_to raise_error
          end
        end
      end
    end
  end
end


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

module Projects
  class DestroyService < BaseService
    include Gitlab::ShellAdapter

    DestroyError = Class.new(StandardError)
    BATCH_SIZE = 100

    def async_execute
      project.update_attribute(:pending_delete, true)

      job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
      log_info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}")
    end

    def execute
      return false unless can?(current_user, :remove_project, project)

      project.update_attribute(:pending_delete, true)

      # There is a possibility of active repository move processes for
      # project and snippets. An attempt to delete the project at the same time
      # can lead to race condition and an inconsistent state.
      #
      # This validation stops the project delete process if it detects active
      # repository move schedules for it.
      validate_active_repositories_move!

      # Flush the cache for both repositories. This has to be done _before_
      # removing the physical repositories as some expiration code depends on
      # Git data (e.g. a list of branch names).
      flush_caches(project)

      ::Ci::AbortPipelinesService.new.execute(project.all_pipelines, :project_deleted)

      Projects::UnlinkForkService.new(project, current_user).execute(refresh_statistics: false)

      attempt_destroy(project)

      system_hook_service.execute_hooks_for(project, :destroy)
      log_info("Project \"#{project.full_path}\" was deleted")

      publish_project_deleted_event_for(project)

      project.invalidate_personal_projects_count_of_owner

      true
    rescue StandardError => error
      context = Gitlab::ApplicationContext.current.merge(project_id: project.id)
      Gitlab::ErrorTracking.track_exception(error, **context)
      attempt_rollback(project, error.message)
      false
    rescue Exception => error # rubocop:disable Lint/RescueException
      # Project.transaction can raise Exception
      attempt_rollback(project, error.message)
      raise
    end

    private

    def validate_active_repositories_move!
      if project.repository_storage_moves.scheduled_or_started.exists?
        raise_error(s_("DeleteProject|Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."))
      end

      if ::ProjectSnippet.by_project(project).with_repository_storage_moves.merge(::Snippets::RepositoryStorageMove.scheduled_or_started).exists?
        raise_error(s_("DeleteProject|Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."))
      end
    end

    def trash_project_repositories!
      unless remove_repository(project.repository)
        raise_error(s_('DeleteProject|Failed to remove project repository. Please try again or contact administrator.'))
      end

      unless remove_repository(project.wiki.repository)
        raise_error(s_('DeleteProject|Failed to remove wiki repository. Please try again or contact administrator.'))
      end

      unless remove_repository(project.design_repository)
        raise_error(s_('DeleteProject|Failed to remove design repository. Please try again or contact administrator.'))
      end
    end

    def trash_relation_repositories!
      unless remove_snippets
        raise_error(s_('DeleteProject|Failed to remove project snippets. Please try again or contact administrator.'))
      end
    end

    def remove_snippets
      # We're setting the skip_authorization param because we dont need to perform the access checks within the service since
      # the user has enough access rights to remove the project and its resources.
      response = ::Snippets::BulkDestroyService.new(current_user, project.snippets).execute(skip_authorization: true)

      if response.error?
        log_error("Snippet deletion failed on #{project.full_path} with the following message: #{response.message}")
      end

      response.success?
    end

    def destroy_events!
      unless remove_events
        raise_error(s_('DeleteProject|Failed to remove events. Please try again or contact administrator.'))
      end
    end

    def remove_events
      log_info("Attempting to destroy events from #{project.full_path} (#{project.id})")

      response = ::Events::DestroyService.new(project).execute

      if response.error?
        log_error("Event deletion failed on #{project.full_path} with the following message: #{response.message}")
      end

      response.success?
    end

    def remove_repository(repository)
      return true unless repository

      result = Repositories::DestroyService.new(repository).execute

      result[:status] == :success
    end

    def attempt_rollback(project, message)
      return unless project

      # It's possible that the project was destroyed, but some after_commit
      # hook failed and caused us to end up here. A destroyed model will be a frozen hash,
      # which cannot be altered.
      unless project.destroyed?
        # Restrict project visibility if the parent group visibility was made more restrictive while the project was scheduled for deletion.
        visibility_level = project.visibility_level_allowed_by_group? ? project.visibility_level : project.group.visibility_level
        project.update(delete_error: message, pending_delete: false, visibility_level: visibility_level)
      end

      log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
    end

    def attempt_destroy(project)
      unless remove_registry_tags
        raise_error(s_('DeleteProject|Failed to remove some tags in project container registry. Please try again or contact administrator.'))
      end

      project.leave_pool_repository
      destroy_project_related_records(project)
    end

    def destroy_project_related_records(project)
      log_destroy_event
      trash_relation_repositories!
      trash_project_repositories!
      destroy_events!
      destroy_web_hooks!
      destroy_project_bots!
      destroy_ci_records!
      destroy_mr_diff_relations!

      destroy_merge_request_diffs!

      # Rails attempts to load all related records into memory before
      # destroying: https://github.com/rails/rails/issues/22510
      # This ensures we delete records in batches.
      #
      # Exclude container repositories because its before_destroy would be
      # called multiple times, and it doesn't destroy any database records.
      project.destroy_dependent_associations_in_batches(exclude: [:container_repositories, :snippets])
      project.destroy!
    end

    def log_destroy_event
      log_info("Attempting to destroy #{project.full_path} (#{project.id})")
    end

    # Projects will have at least one merge_request_diff_commit for every commit
    #   contained in every MR, which deleting via `project.destroy!` and
    #   cascading deletes may exceed statement timeouts, causing failures.
    #   (see https://gitlab.com/gitlab-org/gitlab/-/issues/346166)
    #
    # Removing merge_request_diff_files records may also cause timeouts, so they
    #   can be deleted in batches as well.
    #
    # rubocop: disable CodeReuse/ActiveRecord
    def destroy_mr_diff_relations!
      delete_batch_size = 1000

      project.merge_requests.each_batch(column: :iid, of: BATCH_SIZE) do |relation_ids|
        [MergeRequestDiffCommit, MergeRequestDiffFile].each do |model|
          loop do
            inner_query = model
              .select(:merge_request_diff_id, :relative_order)
              .where(merge_request_diff_id: MergeRequestDiff.where(merge_request_id: relation_ids).select(:id))
              .limit(delete_batch_size)

            deleted_rows = model
              .where("(#{model.table_name}.merge_request_diff_id, #{model.table_name}.relative_order) IN (?)", inner_query) # rubocop:disable GitlabSecurity/SqlInjection
              .delete_all

            break if deleted_rows == 0
          end
        end
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    # rubocop: disable CodeReuse/ActiveRecord
    def destroy_merge_request_diffs!
      delete_batch_size = 1000

      project.merge_requests.each_batch(column: :iid, of: BATCH_SIZE) do |relation|
        loop do
          deleted_rows = MergeRequestDiff
            .where(merge_request: relation)
            .limit(delete_batch_size)
            .delete_all

          break if deleted_rows == 0
        end
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def destroy_ci_records!
      # Make sure to destroy this first just in case the project is undergoing stats refresh.
      # This is to avoid logging the artifact deletion in Ci::JobArtifacts::DestroyBatchService.
      project.build_artifacts_size_refresh&.destroy

      project.all_pipelines.find_each(batch_size: BATCH_SIZE) do |pipeline| # rubocop: disable CodeReuse/ActiveRecord
        # Destroy artifacts, then builds, then pipelines
        # All builds have already been dropped by Ci::AbortPipelinesService,
        # so no Ci::Build-instantiating cancellations happen here.
        # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71342#note_691523196

        ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
      end

      project.secure_files.find_each(batch_size: BATCH_SIZE) do |secure_file| # rubocop: disable CodeReuse/ActiveRecord
        ::Ci::DestroySecureFileService.new(project, current_user).execute(secure_file)
      end

      deleted_count = ::CommitStatus.for_project(project).delete_all

      Gitlab::AppLogger.info(
        class: 'Projects::DestroyService',
        project_id: project.id,
        message: 'leftover commit statuses',
        orphaned_commit_status_count: deleted_count
      )
    end

    # The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
    # By default, they are removed with "DELETE CASCADE" option defined via foreign_key.
    # But such queries can exceed the statement_timeout limit and fail to delete the project.
    # (see https://gitlab.com/gitlab-org/gitlab/-/issues/26259)
    #
    # To prevent that we use WebHooks::DestroyService. It deletes logs in batches and
    # produces smaller and faster queries to the database.
    def destroy_web_hooks!
      project.hooks.find_each do |web_hook|
        result = ::WebHooks::DestroyService.new(current_user).execute(web_hook)

        unless result[:status] == :success
          raise_error(s_('DeleteProject|Failed to remove webhooks. Please try again or contact administrator.'))
        end
      end
    end

    # The project can have multiple project bots with personal access tokens generated.
    # We need to remove them when a project is deleted
    # rubocop: disable CodeReuse/ActiveRecord
    def destroy_project_bots!
      members = project.members
        .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/422405')
        .includes(:user).references(:user).merge(User.project_bot)

      members.each do |member|
        Users::DestroyService.new(current_user).execute(member.user, skip_authorization: true)
      end
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def remove_registry_tags
      return true unless Gitlab.config.registry.enabled
      return false unless remove_legacy_registry_tags

      results = []
      project.container_repositories.find_each do |container_repository|
        results << destroy_repository(project, container_repository)
      end

      results.all?
    end

    ##
    # This method makes sure that we correctly remove registry tags
    # for legacy image repository (when repository path equals project path).
    #
    def remove_legacy_registry_tags
      return true unless Gitlab.config.registry.enabled

      root_repository = ::ContainerRepository.build_root_repository(project)
      root_repository.has_tags? ? destroy_repository(project, root_repository) : true
    end

    def destroy_repository(project, repository)
      service = ContainerRepository::DestroyService.new(project, current_user, { skip_permission_check: true })
      response = service.execute(repository)
      response[:status] == :success
    end

    def raise_error(message)
      raise DestroyError, message
    end

    def flush_caches(project)
      Projects::ForksCountService.new(project).delete_cache
    end

    def publish_project_deleted_event_for(project)
      event = Projects::ProjectDeletedEvent.new(data: {
        project_id: project.id,
        namespace_id: project.namespace_id,
        root_namespace_id: project.root_namespace.id
      })

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

Projects::DestroyService.prepend_mod_with('Projects::DestroyService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher, feature_category: :groups_and_projects do
  include ProjectForksHelper
  include BatchDestroyDependentAssociationsHelper

  let_it_be(:user) { create(:user) }

  let!(:project) { create(:project, :repository, namespace: user.namespace) }
  let(:path) { project.repository.disk_path }
  let(:async) { false } # execute or async_execute

  before do
    stub_container_registry_config(enabled: true)
    stub_container_registry_tags(repository: :any, tags: [])
  end

  shared_examples 'deleting the project' do
    it 'deletes the project', :sidekiq_inline do
      destroy_project(project, user, {})

      expect(Project.unscoped.all).not_to include(project)

      expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
    end

    it 'publishes a ProjectDeletedEvent' do
      expected_data = {
        project_id: project.id,
        namespace_id: project.namespace_id,
        root_namespace_id: project.root_namespace.id
      }

      expect { destroy_project(project, user, {}) }.to publish_event(Projects::ProjectDeletedEvent).with(expected_data)
    end
  end

  shared_examples 'deleting the project with pipeline and build' do
    context 'with pipeline and build related records', :sidekiq_inline do # which has optimistic locking
      let!(:pipeline) { create(:ci_pipeline, project: project) }
      let!(:build) { create(:ci_build, :artifacts, :with_runner_session, pipeline: pipeline) }
      let!(:trace_chunks) { create(:ci_build_trace_chunk, build: build) }
      let!(:job_variables) { create(:ci_job_variable, job: build) }
      let!(:report_result) { create(:ci_build_report_result, build: build) }
      let!(:pending_state) { create(:ci_build_pending_state, build: build) }
      let!(:pipeline_artifact) { create(:ci_pipeline_artifact, pipeline: pipeline) }
      let!(:secure_file) { create(:ci_secure_file, project: project) }

      it 'deletes build and pipeline related records' do
        expect { destroy_project(project, user, {}) }
          .to  change { Ci::Build.count }.by(-1)
          .and change { Ci::BuildTraceChunk.count }.by(-1)
          .and change { Ci::JobArtifact.count }.by(-2)
          .and change { Ci::DeletedObject.count }.by(2)
          .and change { Ci::PipelineArtifact.count }.by(-1)
          .and change { Ci::JobVariable.count }.by(-1)
          .and change { Ci::BuildPendingState.count }.by(-1)
          .and change { Ci::BuildReportResult.count }.by(-1)
          .and change { Ci::BuildRunnerSession.count }.by(-1)
          .and change { Ci::Pipeline.count }.by(-1)
          .and change { Ci::SecureFile.count }.by(-1)
      end

      it 'avoids N+1 queries' do
        recorder = ActiveRecord::QueryRecorder.new { destroy_project(project, user, {}) }

        project = create(:project, :repository, namespace: user.namespace)
        pipeline = create(:ci_pipeline, project: project)
        builds = create_list(:ci_build, 3, :artifacts, pipeline: pipeline)
        create(:ci_pipeline_artifact, pipeline: pipeline)
        create_list(:ci_build_trace_chunk, 3, build: builds[0])

        expect { destroy_project(project, project.first_owner, {}) }.not_to exceed_query_limit(recorder)
      end

      it_behaves_like 'deleting the project'

      context 'when project is undergoing refresh' do
        let!(:build_artifacts_size_refresh) { create(:project_build_artifacts_size_refresh, :pending, project: project) }

        it 'does not log about artifact deletion but continues to delete artifacts' do
          expect(Gitlab::ProjectStatsRefreshConflictsLogger).not_to receive(:warn_artifact_deletion_during_stats_refresh)

          expect { destroy_project(project, user, {}) }
            .to change { Ci::JobArtifact.count }.by(-2)
            .and change { Projects::BuildArtifactsSizeRefresh.count }.by(-1)
        end
      end
    end
  end

  shared_examples 'handles errors thrown during async destroy' do |error_message|
    it 'does not allow the error to bubble up' do
      expect do
        destroy_project(project, user, {})
      end.not_to raise_error
    end

    it 'reports the error' do
      expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
      destroy_project(project, user, {})
    end

    it 'unmarks the project as "pending deletion"' do
      destroy_project(project, user, {})

      expect(project.reload.pending_delete).to be(false)
    end

    it 'stores an error message in `projects.delete_error`' do
      destroy_project(project, user, {})

      expect(project.reload.delete_error).to be_present
      expect(project.delete_error).to match(error_message)
    end

    context 'when parent group visibility was made more restrictive while project was marked "pending deletion"' do
      let!(:group) { create(:group, :public) }
      let!(:project) { create(:project, :repository, :public, namespace: group) }

      it 'sets the project visibility level to that of the parent group' do
        group.add_owner(user)
        project.group.update_attribute(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)

        expect(project.reload.visibility_level).to be(Gitlab::VisibilityLevel::PUBLIC)
        expect(project.group.visibility_level).to be(Gitlab::VisibilityLevel::INTERNAL)

        destroy_project(project, user, {})

        expect(project.reload.visibility_level).to be(Gitlab::VisibilityLevel::INTERNAL)
      end
    end
  end

  context "deleting a project with merge requests" do
    let!(:merge_request) { create(:merge_request, source_project: project) }

    before do
      allow(project).to receive(:destroy!).and_return(true)
    end

    [MergeRequestDiffCommit, MergeRequestDiffFile].each do |model|
      it "deletes #{model} records of the merge request" do
        merge_request_diffs = merge_request.merge_request_diffs
        expect(merge_request_diffs.size).to eq(1)

        records_count = model.where(merge_request_diff_id: merge_request_diffs.first.id).count

        expect { destroy_project(project, user, {}) }.to change { model.count }.by(-records_count)
      end
    end
  end

  context 'deleting a project with merge request diffs' do
    let!(:merge_request) { create(:merge_request, source_project: project) }
    let!(:another_project_mr) { create(:merge_request, source_project: create(:project)) }

    it 'deletes merge request diffs' do
      merge_request_diffs = merge_request.merge_request_diffs
      expect(merge_request_diffs.size).to eq(1)

      expect { destroy_project(project, user, {}) }.to change(MergeRequestDiff, :count).by(-1)
      expect { another_project_mr.reload }.not_to raise_error
    end
  end

  it_behaves_like 'deleting the project'

  context 'personal projects count cache' do
    context 'when the executor is the creator of the project itself' do
      it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
        expect(user).to receive(:invalidate_personal_projects_count)

        destroy_project(project, user, {})
      end
    end

    context 'when the executor is the instance administrator', :enable_admin_mode do
      it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
        expect(user).to receive(:invalidate_personal_projects_count)

        destroy_project(project, create(:admin), {})
      end
    end
  end

  context 'with running pipelines' do
    let!(:pipelines)               { create_list(:ci_pipeline, 3, :running, project: project) }
    let(:destroy_pipeline_service) { double('DestroyPipelineService', execute: nil) }

    it 'bulks-fails with AbortPipelineService and then executes DestroyPipelineService for each pipelines' do
      allow(::Ci::DestroyPipelineService).to receive(:new).and_return(destroy_pipeline_service)

      expect(::Ci::AbortPipelinesService)
        .to receive_message_chain(:new, :execute)
        .with(project.all_pipelines, :project_deleted)

      pipelines.each do |pipeline|
        expect(destroy_pipeline_service).to receive(:execute).with(pipeline)
      end

      destroy_project(project, user, {})
    end
  end

  context 'when project has remote mirrors' do
    let!(:project) do
      create(:project, :repository, namespace: user.namespace).tap do |project|
        project.remote_mirrors.create!(url: 'http://test.com')
      end
    end

    it 'destroys them' do
      expect(RemoteMirror.count).to eq(1)

      destroy_project(project, user, {})

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

  context 'when project has exports' do
    let!(:project_with_export) do
      create(:project, :repository, namespace: user.namespace).tap do |project|
        create(
          :import_export_upload,
          project: project,
          export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz')
        )
      end
    end

    it 'destroys project and export' do
      expect do
        destroy_project(project_with_export, user, {})
      end.to change(ImportExportUpload, :count).by(-1)

      expect(Project.all).not_to include(project_with_export)
    end
  end

  context 'Sidekiq fake' do
    before do
      # Dont run sidekiq to check if renamed repository exists
      Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
    end

    it { expect(Project.all).not_to include(project) }

    it do
      expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
    end
  end

  context 'when flushing caches fail due to Git errors' do
    before do
      allow(project.repository).to receive(:before_delete).and_raise(::Gitlab::Git::CommandError)
      allow(Gitlab::GitLogger).to receive(:warn).with(
        class: Repositories::DestroyService.name,
        container_id: project.id,
        disk_path: project.disk_path,
        message: 'Gitlab::Git::CommandError').and_call_original
    end

    it_behaves_like 'deleting the project'
  end

  context 'when flushing caches fail due to Redis' do
    before do
      new_user = create(:user)
      project.team.add_member(new_user, Gitlab::Access::DEVELOPER)
      allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError)
    end

    it 'keeps project team intact upon an error' do
      perform_enqueued_jobs do
        destroy_project(project, user, {})
      rescue ::Redis::CannotConnectError
      end

      expect(project.team.members.count).to eq 2
    end
  end

  context 'with async_execute', :sidekiq_inline do
    let(:async) { true }

    context 'async delete of project with private issue visibility' do
      before do
        project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
      end

      it_behaves_like 'deleting the project'
    end

    it_behaves_like 'deleting the project with pipeline and build'

    context 'errors' do
      context 'when `remove_legacy_registry_tags` fails' do
        before do
          expect_any_instance_of(described_class)
            .to receive(:remove_legacy_registry_tags).and_return(false)
        end

        it_behaves_like 'handles errors thrown during async destroy', /Failed to remove some tags/
      end

      context 'when `remove_repository` fails' do
        before do
          expect_any_instance_of(described_class)
            .to receive(:remove_repository).and_return(false)
        end

        it_behaves_like 'handles errors thrown during async destroy', /Failed to remove/
      end

      context 'when `execute` raises expected error' do
        before do
          expect_any_instance_of(Project)
            .to receive(:destroy!).and_raise(StandardError.new("Other error message"))
        end

        it_behaves_like 'handles errors thrown during async destroy', /Other error message/
      end

      context 'when `execute` raises unexpected error' do
        before do
          expect_any_instance_of(Project)
            .to receive(:destroy!).and_raise(Exception.new('Other error message'))
        end

        it 'allows error to bubble up and rolls back project deletion' do
          expect do
            destroy_project(project, user, {})
          end.to raise_error(Exception, 'Other error message')

          expect(project.reload.pending_delete).to be(false)
          expect(project.delete_error).to include("Other error message")
        end
      end
    end

    context 'for an archived project' do
      before do
        project.update!(archived: true)
      end

      it_behaves_like 'deleting the project with pipeline and build'
    end
  end

  describe 'container registry' do
    context 'when there are regular container repositories' do
      let(:container_repository) { create(:container_repository) }

      before do
        stub_container_registry_tags(repository: project.full_path + '/image', tags: ['tag'])
        project.container_repositories << container_repository
      end

      context 'when image repository deletion succeeds' do
        it 'returns true' do
          expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |instance|
            expect(instance).to receive(:execute).and_return(status: :success)
          end

          expect(destroy_project(project, user)).to be true
        end
      end

      context 'when image repository deletion raises an error' do
        it 'returns false' do
          expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |service|
            expect(service).to receive(:execute).and_raise(RuntimeError)
          end

          expect(destroy_project(project, user)).to be false
        end
      end

      context 'when image repository deletion fails' do
        it 'returns false' do
          expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
            expect(service).to receive(:execute).and_return({ status: :error })
          end

          expect(destroy_project(project, user)).to be false
        end
      end

      context 'when registry is disabled' do
        before do
          stub_container_registry_config(enabled: false)
        end

        it 'does not attempting to remove any tags' do
          expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)

          destroy_project(project, user)
        end
      end
    end

    context 'when there are tags for legacy root repository' do
      before do
        stub_container_registry_tags(repository: project.full_path, tags: ['tag'])
      end

      context 'when image repository tags deletion succeeds' do
        it 'removes tags' do
          expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
            expect(service).to receive(:execute).and_return({ status: :sucess })
          end

          destroy_project(project, user)
        end
      end

      context 'when image repository tags deletion fails' do
        it 'raises an exception' do
          expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service|
            expect(service).to receive(:execute).and_return({ status: :error })
          end

          expect(destroy_project(project, user)).to be false
        end
      end
    end

    context 'when there are no tags for legacy root repository' do
      before do
        stub_container_registry_tags(repository: project.full_path, tags: [])
      end

      it 'does not try to destroy the repository' do
        expect(Projects::ContainerRepository::DestroyService).not_to receive(:new)

        destroy_project(project, user)
      end
    end
  end

  context 'for a forked project with LFS objects' do
    let(:forked_project) { fork_project(project, user) }

    before do
      project.lfs_objects << create(:lfs_object)
      forked_project.reload
    end

    it 'destroys the fork' do
      expect { destroy_project(forked_project, user) }
        .not_to raise_error
    end

    it 'does not update project statistics for the deleted project' do
      expect(ProjectCacheWorker).not_to receive(:perform_async)

      destroy_project(forked_project, user)
    end
  end

  context 'as the root of a fork network' do
    let!(:fork_1) { fork_project(project, user) }
    let!(:fork_2) { fork_project(project, user) }

    it 'updates the fork network with the project name' do
      fork_network = project.fork_network

      destroy_project(project, user)

      fork_network.reload

      expect(fork_network.deleted_root_project_name).to eq(project.full_name)
      expect(fork_network.root_project).to be_nil
    end
  end

  context 'with related storage move records' do
    context 'when project has active repository storage move records' do
      let!(:project_repository_storage_move) { create(:project_repository_storage_move, :scheduled, container: project) }

      it 'does not delete the project' do
        expect(destroy_project(project, user)).to be_falsey

        expect(project.delete_error).to eq "Couldn't remove the project. A project repository storage move is in progress. Try again when it's complete."
        expect(project.pending_delete).to be_falsey
      end
    end

    context 'when project has active snippet storage move records' do
      let(:project_snippet) { create(:project_snippet, project: project) }
      let!(:snippet_repository_storage_move) { create(:snippet_repository_storage_move, :started, container: project_snippet) }

      it 'does not delete the project' do
        expect(destroy_project(project, user)).to be_falsey

        expect(project.delete_error).to eq "Couldn't remove the project. A related snippet repository storage move is in progress. Try again when it's complete."
        expect(project.pending_delete).to be_falsey
      end
    end
  end

  context 'repository removal' do
    describe '.trash_project_repositories!' do
      let(:trash_project_repositories!) { described_class.new(project, user, {}).send(:trash_project_repositories!) }

      # Destroys 3 repositories:
      # 1. Project repository
      # 2. Wiki repository
      # 3. Design repository

      it 'Repositories::DestroyService is called for existing repos' do
        expect_next_instances_of(Repositories::DestroyService, 3) do |instance|
          expect(instance).to receive(:execute).and_return(status: :success)
        end

        trash_project_repositories!
      end

      context 'when the removal has errors' do
        using RSpec::Parameterized::TableSyntax

        let(:mock_error) { instance_double(Repositories::DestroyService, execute: { message: 'foo', status: :error }) }
        let(:project_repository) { project.repository }
        let(:wiki_repository) { project.wiki.repository }
        let(:design_repository) { project.design_repository }

        where(:repo, :message) do
          ref(:project_repository) | 'Failed to remove project repository. Please try again or contact administrator.'
          ref(:wiki_repository)    | 'Failed to remove wiki repository. Please try again or contact administrator.'
          ref(:design_repository)  | 'Failed to remove design repository. Please try again or contact administrator.'
        end

        with_them do
          before do
            allow(Repositories::DestroyService).to receive(:new).with(anything).and_call_original
            allow(Repositories::DestroyService).to receive(:new).with(repo).and_return(mock_error)
          end

          it 'raises correct error' do
            expect { trash_project_repositories! }.to raise_error(Projects::DestroyService::DestroyError, message)
          end
        end
      end
    end

    it 'removes project repository' do
      expect { destroy_project(project, user, {}) }.to change { project.repository.exists? }.from(true).to(false)
    end

    it 'removes wiki repository' do
      project.create_wiki unless project.wiki.repository.exists?

      expect { destroy_project(project, user, {}) }.to change { project.wiki.repository.exists? }.from(true).to(false)
    end

    it 'removes design repository' do
      project.design_repository.create_if_not_exists

      expect { destroy_project(project, user, {}) }.to change { project.design_repository.exists? }.from(true).to(false)
    end
  end

  context 'snippets' do
    let!(:snippet1) { create(:project_snippet, project: project, author: user) }
    let!(:snippet2) { create(:project_snippet, project: project, author: user) }

    it 'does not include snippets when deleting in batches' do
      expect(project).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:container_repositories, :snippets] })

      destroy_project(project, user)
    end

    it 'calls the bulk snippet destroy service with the skip_authorization param set to true' do
      expect(project.snippets.count).to eq 2

      expect_next_instance_of(Snippets::BulkDestroyService, user, project.snippets) do |instance|
        expect(instance).to receive(:execute).with(skip_authorization: true).and_call_original
      end

      expect do
        destroy_project(project, user)
      end.to change(Snippet, :count).by(-2)
    end

    context 'when an error is raised deleting snippets' do
      let(:error_message) { 'foo' }

      it 'does not delete project' do
        allow_next_instance_of(Snippets::BulkDestroyService) do |instance|
          allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: error_message))
        end

        expect(Gitlab::AppLogger).to receive(:error).with("Snippet deletion failed on #{project.full_path} with the following message: #{error_message}")
        expect(Gitlab::AppLogger).to receive(:error).with(/Failed to remove project snippets/)
        expect(destroy_project(project, user)).to be_falsey
        expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy
      end
    end
  end

  context 'when project has webhooks' do
    let!(:web_hook1) { create(:project_hook, project: project) }
    let!(:web_hook2) { create(:project_hook, project: project) }
    let!(:another_project_web_hook) { create(:project_hook) }
    let!(:web_hook_log) { create(:web_hook_log, web_hook: web_hook1) }

    it 'deletes webhooks and logs related to project' do
      expect_next_instance_of(WebHooks::DestroyService, user) do |instance|
        expect(instance).to receive(:execute).with(web_hook1).and_call_original
      end
      expect_next_instance_of(WebHooks::DestroyService, user) do |instance|
        expect(instance).to receive(:execute).with(web_hook2).and_call_original
      end

      expect do
        destroy_project(project, user)
      end.to change(WebHook, :count).by(-2)
    end

    context 'when an error is raised deleting webhooks' do
      before do
        allow_next_instance_of(WebHooks::DestroyService) do |instance|
          allow(instance).to receive(:execute).and_return(message: 'foo', status: :error)
        end
      end

      it_behaves_like 'handles errors thrown during async destroy', /Failed to remove webhooks/
    end
  end

  context 'when project has project bots' do
    let!(:project_bot) { create(:user, :project_bot).tap { |user| project.add_maintainer(user) } }

    it 'deletes bot user as well' do
      expect_next_instance_of(Users::DestroyService, user) do |instance|
        expect(instance).to receive(:execute).with(project_bot, skip_authorization: true).and_call_original
      end

      destroy_project(project, user)
    end
  end

  context 'when project has events' do
    let!(:event) { create(:event, :created, project: project, target: project, author: user) }

    it 'deletes events from the project' do
      expect do
        destroy_project(project, user)
      end.to change(Event, :count).by(-1)
    end

    context 'when an error is returned while deleting events' do
      it 'does not delete project' do
        allow_next_instance_of(Events::DestroyService) do |instance|
          allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
        end

        expect(destroy_project(project, user)).to be_falsey
        expect(project.delete_error).to include('Failed to remove events')
      end
    end
  end

  context 'error while destroying', :sidekiq_inline do
    let!(:pipeline) { create(:ci_pipeline, project: project) }
    let!(:builds) { create_list(:ci_build, 2, :artifacts, pipeline: pipeline) }
    let!(:build_trace) { create(:ci_build_trace_chunk, build: builds[0]) }

    it 'deletes on retry' do
      # We can expect this to timeout for very large projects
      # TODO: remove allow_next_instance_of: https://gitlab.com/gitlab-org/gitlab/-/issues/220440
      allow_any_instance_of(Ci::Build).to receive(:destroy).and_raise('boom')
      destroy_project(project, user, {})

      allow_any_instance_of(Ci::Build).to receive(:destroy).and_call_original
      destroy_project(project, user, {})

      expect(Project.unscoped.all).not_to include(project)
      expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey
      expect(project.all_pipelines).to be_empty
      expect(project.builds).to be_empty
    end
  end

  context 'associations destoyed in batches' do
    let!(:merge_request) { create(:merge_request, source_project: project) }
    let!(:issue) { create(:issue, project: project) }
    let!(:label) { create(:label, project: project) }

    it 'destroys the associations marked as `dependent: :destroy`, in batches' do
      query_recorder = ActiveRecord::QueryRecorder.new do
        destroy_project(project, user, {})
      end

      expect(project.merge_requests).to be_empty
      expect(project.issues).to be_empty
      expect(project.labels).to be_empty

      expected_queries = [
        delete_in_batches_regexps(:merge_requests, :target_project_id, project, [merge_request]),
        delete_in_batches_regexps(:issues, :project_id, project, [issue]),
        delete_in_batches_regexps(:labels, :project_id, project, [label])
      ].flatten

      expect(query_recorder.log).to include(*expected_queries)
    end
  end

  def destroy_project(project, user, params = {})
    described_class.new(project, user, params).public_send(async ? :async_execute : :execute)
  end
end


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

# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if necessary
module Projects
  class MoveProjectGroupLinksService < BaseMoveRelationsService
    def execute(source_project, remove_remaining_elements: true)
      return unless super

      Project.transaction do
        move_group_links
        remove_remaining_project_group_links if remove_remaining_elements

        success
      end
    end

    private

    def move_group_links
      non_existent_group_links.update_all(project_id: @project.id)
    end

    # Remove remaining project group links from source_project
    def remove_remaining_project_group_links
      source_project.reset.project_group_links.destroy_all # rubocop: disable Cop/DestroyAll
    end

    def group_links_in_target_project
      @project.project_group_links.select(:group_id)
    end

    # Look for groups in source_project that are not in the target project
    # rubocop: disable CodeReuse/ActiveRecord
    def non_existent_group_links
      source_project.project_group_links
                    .where.not(group_id: group_links_in_target_project)
    end
    # rubocop: enable CodeReuse/ActiveRecord
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::MoveProjectGroupLinksService, feature_category: :groups_and_projects do
  let!(:user) { create(:user) }
  let(:project_with_groups) { create(:project, namespace: user.namespace) }
  let(:target_project) { create(:project, namespace: user.namespace) }
  let(:maintainer_group) { create(:group) }
  let(:reporter_group) { create(:group) }
  let(:developer_group) { create(:group) }

  subject { described_class.new(target_project, user) }

  describe '#execute' do
    before do
      project_with_groups.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
      project_with_groups.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
      project_with_groups.project_group_links.create!(group: reporter_group, group_access: Gitlab::Access::REPORTER)
    end

    it 'moves the group links from one project to another' do
      expect(project_with_groups.project_group_links.count).to eq 3
      expect(target_project.project_group_links.count).to eq 0

      subject.execute(project_with_groups)

      expect(project_with_groups.project_group_links.count).to eq 0
      expect(target_project.project_group_links.count).to eq 3
    end

    it 'does not move existent group links in the current project' do
      target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
      target_project.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER)

      expect(project_with_groups.project_group_links.count).to eq 3
      expect(target_project.project_group_links.count).to eq 2

      subject.execute(project_with_groups)

      expect(project_with_groups.project_group_links.count).to eq 0
      expect(target_project.project_group_links.count).to eq 3
    end

    it 'rollbacks changes if transaction fails' do
      allow(subject).to receive(:success).and_raise(StandardError)

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

      expect(project_with_groups.project_group_links.count).to eq 3
      expect(target_project.project_group_links.count).to eq 0
    end

    context 'when remove_remaining_elements is false' do
      let(:options) { { remove_remaining_elements: false } }

      it 'does not remove remaining project group links' do
        target_project.project_group_links.create!(group: maintainer_group, group_access: Gitlab::Access::MAINTAINER)
        target_project.project_group_links.create!(group: developer_group, group_access: Gitlab::Access::DEVELOPER)

        subject.execute(project_with_groups, **options)

        expect(project_with_groups.project_group_links.count).not_to eq 0
      end
    end
  end
end