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

module Issues
  class ExportCsvService < ExportCsv::BaseService
    include Gitlab::Routing.url_helpers
    include GitlabRoutingHelper

    def initialize(relation, resource_parent, user = nil)
      super(relation, resource_parent)

      @labels = objects.labels_hash.transform_values { |labels| labels.sort.join(',').presence }
    end

    def email(mail_to_user)
      Notify.issues_csv_email(mail_to_user, resource_parent, csv_data, csv_builder.status).deliver_now
    end

    private

    def associations_to_preload
      [:work_item_type, :author, :assignees, :timelogs, :milestone, { project: { namespace: :route } }]
    end

    def header_to_value_hash
      {
        'Title' => 'title',
        'Description' => 'description',
        'Issue ID' => 'iid',
        'URL' => -> (issue) { issue_url(issue) },
        'State' => -> (issue) { issue.closed? ? 'Closed' : 'Open' },
        'Author' => 'author_name',
        'Author Username' => -> (issue) { issue.author&.username },
        'Assignee' => -> (issue) { issue.assignees.map(&:name).join(', ') },
        'Assignee Username' => -> (issue) { issue.assignees.map(&:username).join(', ') },
        'Confidential' => -> (issue) { issue.confidential? ? 'Yes' : 'No' },
        'Locked' => -> (issue) { issue.discussion_locked? ? 'Yes' : 'No' },
        'Due Date' => -> (issue) { issue.due_date&.to_fs(:csv) },
        'Created At (UTC)' => -> (issue) { issue.created_at&.to_fs(:csv) },
        'Updated At (UTC)' => -> (issue) { issue.updated_at&.to_fs(:csv) },
        'Closed At (UTC)' => -> (issue) { issue.closed_at&.to_fs(:csv) },
        'Milestone' => -> (issue) { issue.milestone&.title },
        'Weight' => -> (issue) { issue.weight },
        'Labels' => -> (issue) { issue_labels(issue) },
        'Time Estimate' => ->(issue) { issue.time_estimate.to_fs(:csv) },
        'Time Spent' => -> (issue) { issue_time_spent(issue) }
      }
    end

    def issue_labels(issue)
      @labels[issue.id]
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def issue_time_spent(issue)
      issue.timelogs.sum(&:time_spent)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def preload_associations_in_batches?
      Feature.enabled?(:export_csv_preload_in_batches, resource_parent)
    end
  end
end

Issues::ExportCsvService.prepend_mod_with('Issues::ExportCsvService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_planning do
  let_it_be(:user) { create(:user) }
  let_it_be(:group) { create(:group) }
  let_it_be(:project) { create(:project, :public, group: group) }
  let_it_be(:issue) { create(:issue, project: project, author: user) }
  let_it_be(:bad_issue) { create(:issue, project: project, author: user) }

  subject { described_class.new(Issue.all, project) }

  it 'renders csv to string' do
    expect(subject.csv_data).to be_a String
  end

  describe '#email' do
    it 'emails csv' do
      expect { subject.email(user) }.to change(ActionMailer::Base.deliveries, :count)
    end

    it 'renders with a target filesize' do
      expect_next_instance_of(CsvBuilder) do |csv_builder|
        expect(csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE).once
      end

      subject.email(user)
    end
  end

  def csv
    CSV.parse(subject.csv_data, headers: true)
  end

  context 'includes' do
    let_it_be(:milestone) { create(:milestone, title: 'v1.0', project: project) }
    let_it_be(:idea_label) { create(:label, project: project, title: 'Idea') }
    let_it_be(:feature_label) { create(:label, project: project, title: 'Feature') }

    before_all do
      # Creating a timelog touches the updated_at timestamp of issue,
      # so create these first.
      issue.timelogs.create!(time_spent: 360, user: user)
      issue.timelogs.create!(time_spent: 200, user: user)
      issue.update!(
        milestone: milestone,
        assignees: [user],
        description: 'Issue with details',
        state: :opened,
        due_date: DateTime.new(2014, 3, 2),
        created_at: DateTime.new(2015, 4, 3, 2, 1, 0),
        updated_at: DateTime.new(2016, 5, 4, 3, 2, 1),
        closed_at: DateTime.new(2017, 6, 5, 4, 3, 2),
        weight: 4,
        discussion_locked: true,
        labels: [feature_label, idea_label],
        time_estimate: 72000
      )
    end

    shared_examples 'exports CSVs for issues' do
      it 'includes the columns required for import' do
        expect(csv.headers).to include('Title', 'Description')
      end

      it 'returns two issues' do
        expect(csv.count).to eq(2)
      end

      specify 'iid' do
        expect(csv[0]['Issue ID']).to eq issue.iid.to_s
      end

      specify 'url' do
        expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/)
      end

      specify 'title' do
        expect(csv[0]['Title']).to eq issue.title
      end

      specify 'state' do
        expect(csv[0]['State']).to eq 'Open'
      end

      specify 'description' do
        expect(csv[0]['Description']).to eq issue.description
        expect(csv[1]['Description']).to eq nil
      end

      specify 'author name' do
        expect(csv[0]['Author']).to eq issue.author_name
      end

      specify 'author username' do
        expect(csv[0]['Author Username']).to eq issue.author.username
      end

      specify 'assignee name' do
        expect(csv[0]['Assignee']).to eq user.name
        expect(csv[1]['Assignee']).to eq ''
      end

      specify 'assignee username' do
        expect(csv[0]['Assignee Username']).to eq user.username
        expect(csv[1]['Assignee Username']).to eq ''
      end

      specify 'confidential' do
        expect(csv[0]['Confidential']).to eq 'No'
      end

      specify 'milestone' do
        expect(csv[0]['Milestone']).to eq issue.milestone.title
        expect(csv[1]['Milestone']).to eq nil
      end

      specify 'labels' do
        expect(csv[0]['Labels']).to eq 'Feature,Idea'
        expect(csv[1]['Labels']).to eq nil
      end

      specify 'due_date' do
        expect(csv[0]['Due Date']).to eq '2014-03-02'
        expect(csv[1]['Due Date']).to eq nil
      end

      specify 'created_at' do
        expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00'
      end

      specify 'updated_at' do
        expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01'
      end

      specify 'closed_at' do
        expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02'
        expect(csv[1]['Closed At (UTC)']).to eq nil
      end

      specify 'discussion_locked' do
        expect(csv[0]['Locked']).to eq 'Yes'
      end

      specify 'weight' do
        expect(csv[0]['Weight']).to eq '4'
      end

      specify 'time estimate' do
        expect(csv[0]['Time Estimate']).to eq '72000'
        expect(csv[1]['Time Estimate']).to eq '0'
      end

      specify 'time spent' do
        expect(csv[0]['Time Spent']).to eq '560'
        expect(csv[1]['Time Spent']).to eq '0'
      end

      context 'with issues filtered by labels and project' do
        subject do
          described_class.new(
            IssuesFinder.new(user, project_id: project.id, label_name: %w[Idea Feature]).execute,
            project
          )
        end

        it 'returns only filtered objects' do
          expect(csv.count).to eq(1)
          expect(csv[0]['Issue ID']).to eq issue.iid.to_s
        end
      end

      context 'with label links' do
        let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) }

        it 'does not run a query for each label link' do
          control_count = ActiveRecord::QueryRecorder.new { csv }.count

          labeled_issues

          expect { csv }.not_to exceed_query_limit(control_count)
          expect(csv.count).to eq(4)
        end

        it 'returns the labels in sorted order' do
          labeled_issues

          labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) }
          expect(labeled_rows.count).to eq(2)
          expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea"))
        end
      end
    end

    context 'with export_csv_preload_in_batches feature flag disabled' do
      before do
        stub_feature_flags(export_csv_preload_in_batches: false)
      end

      it_behaves_like 'exports CSVs for issues'
    end

    context 'with export_csv_preload_in_batches feature flag enabled' do
      it_behaves_like 'exports CSVs for issues'
    end
  end

  context 'with minimal details' do
    it 'renders labels as nil' do
      expect(csv[0]['Labels']).to eq nil
    end
  end
end


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

module Issues
  class ReferencedMergeRequestsService < Issues::BaseService
    # rubocop: disable CodeReuse/ActiveRecord
    def execute(issue)
      referenced = referenced_merge_requests(issue)
      closed_by = closed_by_merge_requests(issue)

      ActiveRecord::Associations::Preloader.new(
        records: referenced + closed_by,
        associations: { head_pipeline: { project: [:route, { namespace: :route }] } }
      ).call

      [sort_by_iid(referenced), sort_by_iid(closed_by)]
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def referenced_merge_requests(issue)
      merge_requests = extract_merge_requests(issue)

      cross_project_filter = -> (merge_requests) do
        merge_requests.select { |mr| mr.target_project == project }
      end

      Ability.merge_requests_readable_by_user(
        merge_requests,
        current_user,
        filters: {
          read_cross_project: cross_project_filter
        }
      )
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def closed_by_merge_requests(issue)
      return [] unless issue.open?

      merge_requests = extract_merge_requests(issue, filter: :system).select(&:open?)

      return [] if merge_requests.empty?

      ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: issue.id).pluck(:merge_request_id)
      merge_requests.select { |mr| mr.id.in?(ids) }
    end
    # rubocop: enable CodeReuse/ActiveRecord

    private

    def extract_merge_requests(issue, filter: nil)
      ext = issue.all_references(current_user)
      notes = issue_notes(issue)
      notes = notes.select(&filter) if filter

      notes.each do |note|
        note.all_references(current_user, extractor: ext)
      end

      ext.merge_requests
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def issue_notes(issue)
      @issue_notes ||= {}
      @issue_notes[issue] ||= issue.notes.includes(:author)
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def sort_by_iid(merge_requests)
      Gitlab::IssuableSorter.sort(project, merge_requests) { |mr| mr.iid.to_s }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::ReferencedMergeRequestsService, feature_category: :team_planning do
  def create_referencing_mr(attributes = {})
    create(:merge_request, attributes).tap do |merge_request|
      create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
    end
  end

  def create_closing_mr(attributes = {})
    create_referencing_mr(attributes).tap do |merge_request|
      create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
    end
  end

  let_it_be(:user) { create(:user) }
  let_it_be(:project) { create(:project, :public, :repository) }
  let_it_be(:other_project) { create(:project, :public, :repository) }
  let_it_be(:issue) { create(:issue, author: user, project: project) }

  let_it_be(:closing_mr) { create_closing_mr(source_project: project) }
  let_it_be(:closing_mr_other_project) { create_closing_mr(source_project: other_project) }

  let_it_be(:referencing_mr) { create_referencing_mr(source_project: project, source_branch: 'csv') }
  let_it_be(:referencing_mr_other_project) { create_referencing_mr(source_project: other_project, source_branch: 'csv') }

  let(:service) { described_class.new(container: project, current_user: user) }

  describe '#execute' do
    it 'returns a list of sorted merge requests' do
      mrs, closed_by_mrs = service.execute(issue)

      expect(mrs).to eq([closing_mr, referencing_mr, closing_mr_other_project, referencing_mr_other_project])
      expect(closed_by_mrs).to eq([closing_mr, closing_mr_other_project])
    end

    context 'performance' do
      it 'does not run extra queries when extra namespaces are included', :use_clean_rails_memory_store_caching do
        service.execute(issue) # warm cache
        control_count = ActiveRecord::QueryRecorder.new { service.execute(issue) }.count

        third_project = create(:project, :public)
        create_closing_mr(source_project: third_project)
        service.execute(issue) # warm cache

        expect { service.execute(issue) }.not_to exceed_query_limit(control_count)
      end

      it 'preloads the head pipeline for each merge request, and its routes' do
        # Hack to ensure no data is preserved on issue before starting the spec,
        # to avoid false negatives
        reloaded_issue = Issue.find(issue.id)

        pipeline_routes = lambda do |merge_requests|
          merge_requests.map { |mr| mr.head_pipeline&.project&.full_path }
        end

        closing_mr_other_project.update!(head_pipeline: create(:ci_pipeline))
        control_count = ActiveRecord::QueryRecorder.new { service.execute(reloaded_issue).each(&pipeline_routes) }

        closing_mr.update!(head_pipeline: create(:ci_pipeline))

        expect { service.execute(issue).each(&pipeline_routes) }
          .not_to exceed_query_limit(control_count)
      end

      it 'only loads issue notes once' do
        expect(issue).to receive(:notes).once.and_call_original

        service.execute(issue)
      end
    end
  end

  describe '#referenced_merge_requests' do
    it 'returns the referenced merge requests' do
      expect(service.referenced_merge_requests(issue)).to match_array([
                                                                        closing_mr,
                                                                        closing_mr_other_project,
                                                                        referencing_mr,
                                                                        referencing_mr_other_project
                                                                      ])
    end

    it 'excludes cross project references if the user cannot read cross project' do
      allow(Ability).to receive(:allowed?).and_call_original
      expect(Ability).to receive(:allowed?).with(user, :read_cross_project).at_least(:once).and_return(false)

      expect(service.referenced_merge_requests(issue)).not_to include(closing_mr_other_project)
      expect(service.referenced_merge_requests(issue)).not_to include(referencing_mr_other_project)
    end

    context 'performance' do
      it 'does not run a query for each note author', :use_clean_rails_memory_store_caching do
        service.referenced_merge_requests(issue) # warm cache
        control_count = ActiveRecord::QueryRecorder.new { service.referenced_merge_requests(issue) }.count

        create(:note, project: project, noteable: issue, author: create(:user))
        service.referenced_merge_requests(issue) # warm cache

        expect { service.referenced_merge_requests(issue) }.not_to exceed_query_limit(control_count)
      end
    end
  end

  describe '#closed_by_merge_requests' do
    let(:closed_issue) { build(:issue, :closed, project: project) }

    it 'returns the open merge requests that close this issue' do
      create_closing_mr(source_project: project, state: 'closed')

      expect(service.closed_by_merge_requests(issue)).to match_array([closing_mr, closing_mr_other_project])
    end

    it 'returns an empty array when the current issue is closed already' do
      expect(service.closed_by_merge_requests(closed_issue)).to eq([])
    end

    context 'performance' do
      it 'does not run a query for each note author', :use_clean_rails_memory_store_caching do
        service.closed_by_merge_requests(issue) # warm cache
        control_count = ActiveRecord::QueryRecorder.new { service.closed_by_merge_requests(issue) }.count

        create(:note, :system, project: project, noteable: issue, author: create(:user))
        service.closed_by_merge_requests(issue) # warm cache

        expect { service.closed_by_merge_requests(issue) }.not_to exceed_query_limit(control_count)
      end
    end
  end
end


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

# This service fetches all branches containing the current issue's ID, except for
# those with a merge request open referencing the current issue.
module Issues
  class RelatedBranchesService < Issues::BaseService
    def execute(issue)
      branch_names_with_mrs = branches_with_merge_request_for(issue)
      branches = branches_with_iid_of(issue).reject { |b| branch_names_with_mrs.include?(b[:name]) }

      branches.map { |branch| branch_data(branch) }
    end

    private

    def branch_data(branch)
      {
        name: branch[:name],
        pipeline_status: pipeline_status(branch)
      }
    end

    def pipeline_status(branch)
      pipeline = project.latest_pipeline(branch[:name], branch[:target])
      pipeline.detailed_status(current_user) if can?(current_user, :read_pipeline, pipeline)
    end

    def branches_with_merge_request_for(issue)
      Issues::ReferencedMergeRequestsService
        .new(container: project, current_user: current_user)
        .referenced_merge_requests(issue)
        .map(&:source_branch)
    end

    def branches_with_iid_of(issue)
      branch_ref_regex = /\A#{Gitlab::Git::BRANCH_REF_PREFIX}#{issue.iid}-(?!\d+-stable)/i

      return [] unless project.repository.exists?

      project.repository.list_refs(
        [Gitlab::Git::BRANCH_REF_PREFIX + "#{issue.iid}-*"]
      ).each_with_object([]) do |ref, results|
        if ref.name.match?(branch_ref_regex)
          results << { name: ref.name.delete_prefix(Gitlab::Git::BRANCH_REF_PREFIX), target: ref.target }
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::RelatedBranchesService, feature_category: :team_planning do
  let_it_be(:project) { create(:project, :repository, :public, public_builds: false) }
  let_it_be(:developer) { create(:user) }
  let_it_be(:issue) { create(:issue, project: project) }

  let(:user) { developer }

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

  before_all do
    project.add_developer(developer)
  end

  describe '#execute' do
    let(:branch_info) { subject.execute(issue) }

    context 'branches are available' do
      let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project, ref: issue.to_branch_name) }

      before_all do
        project.repository.create_branch(issue.to_branch_name, pipeline.sha)
        project.repository.create_branch("#{issue.iid}doesnt-match", project.repository.root_ref)
        project.repository.create_branch("#{issue.iid}-0-stable", project.repository.root_ref)

        project.repository.add_tag(developer, issue.to_branch_name, pipeline.sha)
      end

      context 'when user has access to pipelines' do
        it 'selects relevant branches, along with pipeline status' do
          expect(branch_info).to contain_exactly(
            { name: issue.to_branch_name, pipeline_status: an_instance_of(Gitlab::Ci::Status::Success) }
          )
        end
      end

      context 'when user does not have access to pipelines' do
        let(:user) { create(:user) }

        it 'returns branches without pipeline status' do
          expect(branch_info).to contain_exactly(
            { name: issue.to_branch_name, pipeline_status: nil }
          )
        end
      end

      it 'excludes branches referenced in merge requests' do
        merge_request = create(:merge_request, { description: "Closes #{issue.to_reference}",
                                                 source_project: issue.project,
                                                 source_branch: issue.to_branch_name })
        merge_request.create_cross_references!(user)

        referenced_merge_requests = Issues::ReferencedMergeRequestsService
                                      .new(container: issue.project, current_user: user)
                                      .referenced_merge_requests(issue)

        expect(referenced_merge_requests).not_to be_empty
        expect(branch_info.pluck(:name)).not_to include(merge_request.source_branch)
      end
    end

    context 'no branches are available' do
      let(:project) { create(:project, :empty_repo) }

      it 'returns an empty array' do
        expect(branch_info).to be_empty
      end
    end
  end
end


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

module Issues
  class ReopenService < Issues::BaseService
    def execute(issue, skip_authorization: false)
      return issue unless can_reopen?(issue, skip_authorization: skip_authorization)

      if issue.reopen
        event_service.reopen_issue(issue, current_user)

        if current_user.project_bot?
          log_audit_event(issue, current_user, "#{issue.issue_type}_reopened_by_project_bot",
            "Reopened #{issue.issue_type.humanize(capitalize: false)} #{issue.title}")
        end

        create_note(issue, 'reopened')
        notification_service.async.reopen_issue(issue, current_user)
        perform_incident_management_actions(issue)
        execute_hooks(issue, 'reopen')
        invalidate_cache_counts(issue, users: issue.assignees)
        issue.update_project_counter_caches
        Milestones::ClosedIssuesCountService.new(issue.milestone).delete_cache if issue.milestone
        track_incident_action(current_user, issue, :incident_reopened)
      end

      issue
    end

    private

    def can_reopen?(issue, skip_authorization: false)
      skip_authorization || can?(current_user, :reopen_issue, issue)
    end

    def perform_incident_management_actions(issue)
      return unless issue.work_item_type&.incident?

      create_timeline_event(issue)
    end

    def create_note(issue, state = issue.state)
      SystemNoteService.change_status(issue, issue.project, current_user, state, nil)
    end

    def create_timeline_event(issue)
      IncidentManagement::TimelineEvents::CreateService.reopen_incident(issue, current_user)
    end
  end
end

Issues::ReopenService.prepend_mod_with('Issues::ReopenService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::ReopenService, feature_category: :team_planning do
  let(:project) { create(:project) }
  let(:issue) { create(:issue, :closed, project: project) }

  describe '#execute' do
    context 'when user is not authorized to reopen issue' do
      it 'does not reopen the issue' do
        guest = create(:user)
        project.add_guest(guest)

        described_class.new(container: project, current_user: guest).execute(issue)

        expect(issue).to be_closed
      end

      context 'when skip_authorization is true' do
        it 'does close the issue even if user is not authorized' do
          non_authorized_user = create(:user)

          service = described_class.new(container: project, current_user: non_authorized_user)

          expect do
            service.execute(issue, skip_authorization: true)
          end.to change { issue.reload.state }.from('closed').to('opened')
        end
      end
    end

    context 'when user is authorized to reopen issue' do
      let(:user) { create(:user) }

      subject(:execute) { described_class.new(container: project, current_user: user).execute(issue) }

      before do
        project.add_maintainer(user)
      end

      it 'invalidates counter cache for assignees' do
        issue.assignees << user
        expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts)

        execute
      end

      it 'refreshes the number of opened issues' do
        expect do
          execute

          BatchLoader::Executor.clear_current
        end.to change { project.open_issues_count }.from(0).to(1)
      end

      it 'deletes milestone issue counters cache' do
        issue.update!(milestone: create(:milestone, project: project))

        expect_next_instance_of(Milestones::ClosedIssuesCountService, issue.milestone) do |service|
          expect(service).to receive(:delete_cache).and_call_original
        end

        execute
      end

      it 'does not create timeline event' do
        expect { execute }.not_to change { issue.incident_management_timeline_events.count }
      end

      it 'does not call GroupMentionWorker' do
        expect(Integrations::GroupMentionWorker).not_to receive(:perform_async)

        issue
      end

      context 'issue is incident type' do
        let(:issue) { create(:incident, :closed, project: project) }
        let(:current_user) { user }

        it_behaves_like 'an incident management tracked event', :incident_management_incident_reopened

        it_behaves_like 'Snowplow event tracking with RedisHLL context' do
          let(:namespace) { issue.namespace }
          let(:category) { described_class.to_s }
          let(:action) { 'incident_management_incident_reopened' }
          let(:label) { 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly' }
        end

        it 'creates a timeline event' do
          expect(IncidentManagement::TimelineEvents::CreateService)
            .to receive(:reopen_incident)
            .with(issue, current_user)
            .and_call_original

          expect { execute }.to change { issue.incident_management_timeline_events.count }.by(1)
        end
      end

      context 'when issue is not confidential' do
        let(:expected_payload) do
          include(
            event_type: 'issue',
            object_kind: 'issue',
            changes: {
              closed_at: { current: nil, previous: kind_of(Time) },
              state_id: { current: 1, previous: 2 },
              updated_at: { current: kind_of(Time), previous: kind_of(Time) }
            },
            object_attributes: include(
              state: 'opened',
              action: 'reopen'
            )
          )
        end

        it 'executes issue hooks' do
          expect(project.project_namespace).to receive(:execute_hooks).with(expected_payload, :issue_hooks)
          expect(project.project_namespace).to receive(:execute_integrations).with(expected_payload, :issue_hooks)

          execute
        end
      end

      context 'when issue is confidential' do
        let(:issue) { create(:issue, :confidential, :closed, project: project) }

        it 'executes confidential issue hooks' do
          issue_hooks = :confidential_issue_hooks
          expect(project.project_namespace).to receive(:execute_hooks).with(an_instance_of(Hash), issue_hooks)
          expect(project.project_namespace).to receive(:execute_integrations).with(an_instance_of(Hash), issue_hooks)

          execute
        end
      end
    end
  end
end


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

module Issues
  class DuplicateService < Issues::BaseService
    def execute(duplicate_issue, canonical_issue)
      return if canonical_issue == duplicate_issue
      return unless can?(current_user, :update_issue, duplicate_issue)
      return unless can?(current_user, :create_note, canonical_issue)

      create_issue_duplicate_note(duplicate_issue, canonical_issue)
      create_issue_canonical_note(canonical_issue, duplicate_issue)

      close_service.new(container: project, current_user: current_user).execute(duplicate_issue)
      duplicate_issue.update(duplicated_to: canonical_issue)

      relate_two_issues(duplicate_issue, canonical_issue)
    end

    private

    def create_issue_duplicate_note(duplicate_issue, canonical_issue)
      SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue)
    end

    def create_issue_canonical_note(canonical_issue, duplicate_issue)
      SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
    end

    def relate_two_issues(duplicate_issue, canonical_issue)
      params = { target_issuable: canonical_issue }

      IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::DuplicateService, feature_category: :team_planning do
  let(:user) { create(:user) }
  let(:canonical_project) { create(:project) }
  let(:duplicate_project) { create(:project) }

  let(:canonical_issue) { create(:issue, project: canonical_project) }
  let(:duplicate_issue) { create(:issue, project: duplicate_project) }

  subject { described_class.new(container: duplicate_project, current_user: user) }

  describe '#execute' do
    context 'when the issues passed are the same' do
      it 'does nothing' do
        expect(subject).not_to receive(:close_service)
        expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
        expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)

        subject.execute(duplicate_issue, duplicate_issue)
      end
    end

    context 'when the user cannot update the duplicate issue' do
      before do
        canonical_project.add_reporter(user)
      end

      it 'does nothing' do
        expect(subject).not_to receive(:close_service)
        expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
        expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)

        subject.execute(duplicate_issue, canonical_issue)
      end
    end

    context 'when the user cannot comment on the canonical issue' do
      before do
        duplicate_project.add_reporter(user)
      end

      it 'does nothing' do
        expect(subject).not_to receive(:close_service)
        expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
        expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)

        subject.execute(duplicate_issue, canonical_issue)
      end
    end

    context 'when the user can mark the issue as a duplicate' do
      before do
        canonical_project.add_reporter(user)
        duplicate_project.add_reporter(user)
      end

      it 'closes the duplicate issue' do
        subject.execute(duplicate_issue, canonical_issue)

        expect(duplicate_issue.reload).to be_closed
        expect(canonical_issue.reload).to be_open
      end

      it 'adds a system note to the duplicate issue' do
        expect(SystemNoteService)
          .to receive(:mark_duplicate_issue).with(duplicate_issue, duplicate_project, user, canonical_issue)

        subject.execute(duplicate_issue, canonical_issue)
      end

      it 'adds a system note to the canonical issue' do
        expect(SystemNoteService)
          .to receive(:mark_canonical_issue_of_duplicate).with(canonical_issue, canonical_project, user, duplicate_issue)

        subject.execute(duplicate_issue, canonical_issue)
      end

      it 'updates duplicate issue with canonical issue id' do
        subject.execute(duplicate_issue, canonical_issue)

        expect(duplicate_issue.reload.duplicated_to).to eq(canonical_issue)
      end

      it 'relates the duplicate issues' do
        canonical_project.add_reporter(user)
        duplicate_project.add_reporter(user)

        subject.execute(duplicate_issue, canonical_issue)

        issue_link = IssueLink.last
        expect(issue_link.source).to eq(duplicate_issue)
        expect(issue_link.target).to eq(canonical_issue)
      end
    end
  end
end


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

module Issues
  class SetCrmContactsService < ::BaseProjectService
    MAX_ADDITIONAL_CONTACTS = 6

    # Replacing contacts by email is not currently supported
    def execute(issue)
      @issue = issue
      @errors = []

      return error_no_permissions unless allowed?
      return error_invalid_params unless valid_params?

      @existing_ids = issue.customer_relations_contact_ids
      determine_changes if set_present?
      return error_too_many if too_many?

      @added_count = 0
      @removed_count = 0

      add if params[:add_ids].present?
      remove if params[:remove_ids].present?

      add_by_email if params[:add_emails].present?
      remove_by_email if params[:remove_emails].present?

      if issue.valid?
        GraphqlTriggers.issue_crm_contacts_updated(issue)
        issue.touch
        create_system_note
        ServiceResponse.success(payload: issue)
      else
        # The default error isn't very helpful: "Issue customer relations contacts is invalid"
        issue.errors.delete(:issue_customer_relations_contacts)
        issue.errors.add(:issue_customer_relations_contacts, errors.to_sentence)
        ServiceResponse.error(payload: issue, message: issue.errors.full_messages.to_sentence)
      end
    end

    private

    attr_accessor :issue, :errors, :existing_ids, :added_count, :removed_count

    def determine_changes
      params[:add_ids] = params[:replace_ids] - existing_ids
      params[:remove_ids] = existing_ids - params[:replace_ids]
    end

    def add
      add_by_id(params[:add_ids])
    end

    def add_by_email
      contact_ids = ::CustomerRelations::Contact.find_ids_by_emails(project_group.root_ancestor, emails(:add_emails))
      add_by_id(contact_ids)
    end

    def emails(key)
      params[key].map do |email|
        extract_email_from_request_param(email)
      end
    end

    def add_by_id(contact_ids)
      contact_ids -= existing_ids
      contact_ids.uniq.each do |contact_id|
        issue_contact = issue.issue_customer_relations_contacts.create(contact_id: contact_id)

        if issue_contact.persisted?
          @added_count += 1
        else
          # The validation ensures that the id exists and the user has permission
          errors << "#{contact_id}: The resource that you are attempting to access does not exist or you don't have permission to perform this action"
        end
      end
    end

    def remove
      remove_by_id(params[:remove_ids])
    end

    def remove_by_email
      contact_ids = ::CustomerRelations::IssueContact.find_contact_ids_by_emails(issue.id, emails(:remove_emails))
      remove_by_id(contact_ids)
    end

    def remove_by_id(contact_ids)
      contact_ids &= existing_ids
      @removed_count += issue.issue_customer_relations_contacts
        .where(contact_id: contact_ids) # rubocop: disable CodeReuse/ActiveRecord
        .delete_all
    end

    def extract_email_from_request_param(email_param)
      email_param.delete_prefix(::CustomerRelations::Contact.reference_prefix_quoted)
                 .delete_prefix(::CustomerRelations::Contact.reference_prefix)
                 .delete_suffix(::CustomerRelations::Contact.reference_postfix)
                 .tr('"', '')
    end

    def allowed?
      current_user&.can?(:set_issue_crm_contacts, issue)
    end

    def valid_params?
      set_present? ^ add_or_remove_present?
    end

    def set_present?
      !params[:replace_ids].nil?
    end

    def add_or_remove_present?
      add_present? || remove_present?
    end

    def add_present?
      params[:add_ids].present? || params[:add_emails].present?
    end

    def remove_present?
      params[:remove_ids].present? || params[:remove_emails].present?
    end

    def too_many?
      too_many_ids? || too_many_emails?
    end

    def too_many_ids?
      params[:add_ids] && params[:add_ids].length > MAX_ADDITIONAL_CONTACTS
    end

    def too_many_emails?
      params[:add_emails] && params[:add_emails].length > MAX_ADDITIONAL_CONTACTS
    end

    def create_system_note
      SystemNoteService.change_issuable_contacts(
        issue, issue.project, current_user, added_count, removed_count)
    end

    def error_no_permissions
      ServiceResponse.error(message: _('You have insufficient permissions to set customer relations contacts for this issue'))
    end

    def error_invalid_params
      ServiceResponse.error(message: _('You cannot combine replace_ids with add_ids or remove_ids'))
    end

    def error_too_many
      ServiceResponse.error(payload: issue, message: _("You can only add up to %{max_contacts} contacts at one time" % { max_contacts: MAX_ADDITIONAL_CONTACTS }))
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::SetCrmContactsService, feature_category: :team_planning do
  let_it_be(:user) { create(:user) }
  let_it_be(:group) { create(:group, :crm_enabled) }
  let_it_be(:project) { create(:project, group: create(:group, :crm_enabled, parent: group)) }
  let_it_be(:contacts) { create_list(:contact, 4, group: group) }
  let_it_be(:issue, reload: true) { create(:issue, project: project) }
  let_it_be(:issue_contact_1) do
    create(:issue_customer_relations_contact, issue: issue, contact: contacts[0]).contact
  end

  let_it_be(:issue_contact_2) do
    create(:issue_customer_relations_contact, issue: issue, contact: contacts[1]).contact
  end

  let(:does_not_exist_or_no_permission) { "The resource that you are attempting to access does not exist or you don't have permission to perform this action" }

  subject(:set_crm_contacts) do
    described_class.new(project: project, current_user: user, params: params).execute(issue)
  end

  describe '#execute' do
    shared_examples 'setting contacts' do
      it 'updates the issue with correct contacts' do
        response = set_crm_contacts

        expect(response).to be_success
        expect(issue.customer_relations_contacts).to match_array(expected_contacts)
      end
    end

    shared_examples 'adds system note' do |added_count, removed_count|
      it 'calls SystemNoteService.change_issuable_contacts with correct counts' do
        expect(SystemNoteService)
          .to receive(:change_issuable_contacts)
          .with(issue, project, user, added_count, removed_count)

        set_crm_contacts
      end
    end

    context 'when the user has no permission' do
      let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }

      it 'returns expected error response' do
        response = set_crm_contacts

        expect(response).to be_error
        expect(response.message).to eq('You have insufficient permissions to set customer relations contacts for this issue')
      end
    end

    context 'when user has permission' do
      before do
        group.add_reporter(user)
      end

      context 'but the crm setting is disabled' do
        let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
        let(:subgroup_with_crm_disabled) { create(:group, parent: group) }
        let(:project_with_crm_disabled) { create(:project, group: subgroup_with_crm_disabled) }
        let(:issue_with_crm_disabled) { create(:issue, project: project_with_crm_disabled) }

        it 'returns expected error response' do
          response = described_class.new(project: project_with_crm_disabled, current_user: user, params: params).execute(issue_with_crm_disabled)

          expect(response).to be_error
          expect(response.message).to eq('You have insufficient permissions to set customer relations contacts for this issue')
        end
      end

      context 'when the contact does not exist' do
        let(:params) { { replace_ids: [non_existing_record_id] } }

        it 'returns expected error response' do
          response = set_crm_contacts

          expect(response).to be_error
          expect(response.message).to eq("Issue customer relations contacts #{non_existing_record_id}: #{does_not_exist_or_no_permission}")
        end
      end

      context 'when the contact belongs to a different group' do
        let(:group2) { create(:group) }
        let(:contact) { create(:contact, group: group2) }
        let(:params) { { replace_ids: [contact.id] } }

        before do
          group2.add_reporter(user)
        end

        it 'returns expected error response' do
          response = set_crm_contacts

          expect(response).to be_error
          expect(response.message).to eq("Issue customer relations contacts #{contact.id}: #{does_not_exist_or_no_permission}")
        end
      end

      context 'replace' do
        let(:params) { { replace_ids: [contacts[1].id, contacts[2].id] } }
        let(:expected_contacts) { [contacts[1], contacts[2]] }

        it_behaves_like 'setting contacts'
        it_behaves_like 'adds system note', 1, 1

        context 'with empty list' do
          let(:params) { { replace_ids: [] } }
          let(:expected_contacts) { [] }

          it_behaves_like 'setting contacts'
          it_behaves_like 'adds system note', 0, 2
        end
      end

      context 'add' do
        let(:added_contact) { contacts[3] }
        let(:params) { { add_ids: [added_contact.id] } }
        let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }

        it_behaves_like 'setting contacts'
        it_behaves_like 'adds system note', 1, 0
      end

      context 'add by email' do
        let(:added_contact) { contacts[3] }
        let(:expected_contacts) { [issue_contact_1, issue_contact_2, added_contact] }

        context 'with pure emails in params' do
          let(:params) { { add_emails: [contacts[3].email] } }

          it_behaves_like 'setting contacts'
          it_behaves_like 'adds system note', 1, 0
        end

        context 'with autocomplete prefix emails in params' do
          let(:params) { { add_emails: ["[\"contact:\"#{contacts[3].email}\"]"] } }

          it_behaves_like 'setting contacts'
          it_behaves_like 'adds system note', 1, 0
        end
      end

      context 'remove' do
        let(:params) { { remove_ids: [contacts[0].id] } }
        let(:expected_contacts) { [contacts[1]] }

        it_behaves_like 'setting contacts'
        it_behaves_like 'adds system note', 0, 1
      end

      context 'remove by email' do
        let(:expected_contacts) { [contacts[1]] }

        context 'with pure email in params' do
          let(:params) { { remove_emails: [contacts[0].email] } }

          it_behaves_like 'setting contacts'
          it_behaves_like 'adds system note', 0, 1
        end

        context 'with autocomplete prefix and suffix email in params' do
          let(:params) { { remove_emails: ["[contact:#{contacts[0].email}]"] } }

          it_behaves_like 'setting contacts'
          it_behaves_like 'adds system note', 0, 1
        end
      end

      context 'when attempting to add more than 6' do
        let(:id) { contacts[0].id }
        let(:params) { { add_ids: [id, id, id, id, id, id, id] } }

        it 'returns expected error message' do
          response = set_crm_contacts

          expect(response).to be_error
          expect(response.message).to eq('You can only add up to 6 contacts at one time')
        end
      end

      context 'when trying to remove non-existent contact' do
        let(:params) { { remove_ids: [non_existing_record_id] } }

        it 'returns expected error message' do
          response = set_crm_contacts

          expect(response).to be_success
          expect(response.message).to be_nil
        end
      end

      context 'when combining params' do
        let(:error_invalid_params) { 'You cannot combine replace_ids with add_ids or remove_ids' }
        let(:expected_contacts) { [contacts[0], contacts[3]] }

        context 'add and remove' do
          context 'with contact ids' do
            let(:params) { { remove_ids: [contacts[1].id], add_ids: [contacts[3].id] } }

            it_behaves_like 'setting contacts'
          end

          context 'with contact emails' do
            let(:params) { { remove_emails: [contacts[1].email], add_emails: ["[\"contact:#{contacts[3].email}]"] } }

            it_behaves_like 'setting contacts'
          end
        end

        context 'replace and remove' do
          let(:params) { { replace_ids: [contacts[3].id], remove_ids: [contacts[0].id] } }

          it 'returns expected error response' do
            response = set_crm_contacts

            expect(response).to be_error
            expect(response.message).to eq(error_invalid_params)
          end
        end

        context 'replace and add' do
          let(:params) { { replace_ids: [contacts[3].id], add_ids: [contacts[1].id] } }

          it 'returns expected error response' do
            response = set_crm_contacts

            expect(response).to be_error
            expect(response.message).to eq(error_invalid_params)
          end
        end
      end

      context 'when trying to add an existing issue contact' do
        let(:params) { { add_ids: [contacts[0].id] } }

        it 'does not return an error' do
          response = set_crm_contacts

          expect(response).to be_success
        end
      end

      context 'when trying to add the same contact twice' do
        let(:params) { { add_ids: [contacts[3].id, contacts[3].id] } }

        it 'does not return an error' do
          response = set_crm_contacts

          expect(response).to be_success
        end
      end

      context 'when trying to remove a contact not attached to the issue' do
        let(:params) { { remove_ids: [contacts[3].id] } }

        it 'does not return an error' do
          response = set_crm_contacts

          expect(response).to be_success
        end
      end
    end
  end
end


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

module Issues
  class RelativePositionRebalancingService
    UPDATE_BATCH_SIZE = 100
    PREFETCH_ISSUES_BATCH_SIZE = 10_000
    SMALLEST_BATCH_SIZE = 5
    RETRIES_LIMIT = 3

    TooManyConcurrentRebalances = Class.new(StandardError)

    def initialize(projects)
      @projects_collection = (projects.is_a?(Array) ? Project.id_in(projects) : projects).select(:id).projects_order_id_asc
      @root_namespace = @projects_collection.select(:namespace_id).reorder(nil).take.root_namespace # rubocop:disable CodeReuse/ActiveRecord
      @caching = ::Gitlab::Issues::Rebalancing::State.new(@root_namespace, @projects_collection)
    end

    def execute
      # Given can_start_rebalance? and track_new_running_rebalance are not atomic
      # it can happen that we end up with more than Rebalancing::State::MAX_NUMBER_OF_CONCURRENT_REBALANCES running.
      # Considering the number of allowed Rebalancing::State::MAX_NUMBER_OF_CONCURRENT_REBALANCES is small we should be ok,
      # but should be something to consider if we'd want to scale this up.
      error_message = "#{caching.concurrent_running_rebalances_count} concurrent re-balances currently running"
      raise TooManyConcurrentRebalances, error_message unless caching.can_start_rebalance?

      block_issue_repositioning! unless root_namespace.issue_repositioning_disabled?
      caching.track_new_running_rebalance
      index = caching.get_current_index

      loop do
        issue_ids = get_issue_ids(index, PREFETCH_ISSUES_BATCH_SIZE)
        pairs_with_index = assign_indexes(issue_ids, index)

        pairs_with_index.each_slice(UPDATE_BATCH_SIZE) do |pairs_batch|
          update_positions_with_retry(pairs_batch, 're-balance issue positions in batches ordered by position')
        end

        index = caching.get_current_index

        break if index >= caching.issue_count - 1
      end

      caching.cleanup_cache
      unblock_issue_repositioning!
    end

    private

    attr_reader :root_namespace, :projects_collection, :caching

    def block_issue_repositioning!
      Feature.enable(:block_issue_repositioning, root_namespace)
    end

    def unblock_issue_repositioning!
      Feature.disable(:block_issue_repositioning, root_namespace)
    end

    def get_issue_ids(index, limit)
      issue_ids = caching.get_cached_issue_ids(index, limit)

      # if we have a list of cached issues and no current project id cached,
      # then we successfully cached issues for all projects
      return issue_ids if issue_ids.any? && caching.get_current_project_id.blank?

      # if we got no issue ids at the start of re-balancing then we did not cache any issue ids yet
      preload_issue_ids

      caching.get_cached_issue_ids(index, limit)
    end

    # rubocop: disable CodeReuse/ActiveRecord
    def preload_issue_ids
      index = 0
      cached_project_id = caching.get_current_project_id

      collection = projects_collection
      collection = projects_collection.where(Project.arel_table[:id].gteq(cached_project_id.to_i)) if cached_project_id.present?

      collection.each do |project|
        caching.cache_current_project_id(project.id)
        index += 1
        scope = Issue.in_projects(project).order_by_relative_position.with_non_null_relative_position.select(:id, :relative_position)

        with_retry(PREFETCH_ISSUES_BATCH_SIZE, 100) do |batch_size|
          Gitlab::Pagination::Keyset::Iterator.new(scope: scope).each_batch(of: batch_size) do |batch|
            caching.cache_issue_ids(batch)
          end
        end
      end

      caching.remove_current_project_id_cache
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def assign_indexes(ids, start_index)
      ids.each_with_index.map do |id, idx|
        [id, start_index + idx]
      end
    end

    # The method runs in a loop where we try for RETRIES_LIMIT=3 times, to run the update statement on
    # a number of records(batch size). Method gets an array of (id, value) pairs as argument that is used
    # to build the update query matching by id and updating relative_position = value. If we get a statement
    # timeout, we split the batch size in half and try(for 3 times again) to batch update on a smaller number of records.
    # On success, because we know the batch size and we always pick from the beginning of the array param,
    # we can remove first batch_size number of items from array and continue with the successful batch_size for next batches.
    # On failures we continue to split batch size to a SMALLEST_BATCH_SIZE limit, which is now set at 5.
    #
    # e.g.
    # 0. items | previous batch size|new batch size | comment
    # 1. 100   | 100                | 100           | 3 failures -> split the batch size in half
    # 2. 100   | 100                | 50            | 3 failures -> split the batch size in half again
    # 3. 100   | 50                 | 25            | 3 succeed -> so we drop 25 items 3 times, 4th fails -> split the batch size in half again
    # 5. 25    | 25                 | 12            | 3 failures -> split the batch size in half
    # 6. 25    | 12                 | 6             | 3 failures -> we exit because smallest batch size is 5 and we'll be at 3 if we split again

    def update_positions_with_retry(pairs_with_index, query_name)
      retry_batch_size = pairs_with_index.size

      until pairs_with_index.empty?
        with_retry(retry_batch_size, SMALLEST_BATCH_SIZE) do |batch_size|
          retry_batch_size = batch_size
          update_positions(pairs_with_index.first(batch_size), query_name)
          # pairs_with_index[batch_size - 1] - can be nil for last batch
          # if last batch is smaller than batch_size, so we just get the last pair.
          last_pair_in_batch = pairs_with_index[batch_size - 1] || pairs_with_index.last
          caching.cache_current_index(last_pair_in_batch.last + 1)
          pairs_with_index = pairs_with_index.drop(batch_size)
        end
      end
    end

    def update_positions(pairs_with_position, query_name)
      values = pairs_with_position.map do |id, index|
        "(#{id}, #{start_position + (index * gap_size)})"
      end.join(', ')

      run_update_query(values, query_name)
    end

    def run_update_query(values, query_name)
      Issue.connection.exec_query(<<~SQL, query_name)
        WITH cte(cte_id, new_pos) AS MATERIALIZED (
         SELECT *
         FROM (VALUES #{values}) as t (id, pos)
        )
        UPDATE #{Issue.table_name}
        SET relative_position = cte.new_pos
        FROM cte
        WHERE cte_id = id
      SQL
    end

    def gaps
      caching.issue_count - 1
    end

    def gap_size
      RelativePositioning::MAX_GAP
    end

    def start_position
      @start_position ||= (RelativePositioning::START_POSITION - (gaps / 2) * gap_size).to_i
    end

    def with_retry(initial_batch_size, exit_batch_size)
      retries = 0
      batch_size = initial_batch_size

      begin
        yield batch_size
        retries = 0
      rescue ActiveRecord::StatementTimeout, ActiveRecord::QueryCanceled => ex
        raise ex if batch_size < exit_batch_size

        if (retries += 1) == RETRIES_LIMIT
          # shrink the batch size in half when RETRIES limit is reached and update still fails perhaps because batch size is still too big
          batch_size = (batch_size / 2).to_i
          retries = 0
        end

        retry
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::RelativePositionRebalancingService, :clean_gitlab_redis_shared_state, feature_category: :team_planning do
  let_it_be(:project, reload: true) { create(:project, :repository_disabled, skip_disk_validation: true) }
  let_it_be(:user) { project.creator }
  let_it_be(:start) { RelativePositioning::START_POSITION }
  let_it_be(:max_pos) { RelativePositioning::MAX_POSITION }
  let_it_be(:min_pos) { RelativePositioning::MIN_POSITION }
  let_it_be(:clump_size) { 300 }

  let_it_be(:unclumped, reload: true) do
    (1..clump_size).to_a.map do |i|
      create(:issue, project: project, author: user, relative_position: start + (1024 * i))
    end
  end

  let_it_be(:end_clump, reload: true) do
    (1..clump_size).to_a.map do |i|
      create(:issue, project: project, author: user, relative_position: max_pos - i)
    end
  end

  let_it_be(:start_clump, reload: true) do
    (1..clump_size).to_a.map do |i|
      create(:issue, project: project, author: user, relative_position: min_pos + i)
    end
  end

  let_it_be(:nil_clump, reload: true) do
    (1..100).to_a.map do |i|
      create(:issue, project: project, author: user, relative_position: nil)
    end
  end

  def issues_in_position_order
    project.reload.issues.order_by_relative_position.to_a
  end

  subject(:service) { described_class.new(Project.id_in(project)) }

  context 'execute' do
    it 're-balances a set of issues with clumps at the end and start' do
      all_issues = start_clump + unclumped + end_clump.reverse

      expect { service.execute }.not_to change { issues_in_position_order.map(&:id) }

      caching = service.send(:caching)
      all_issues.each(&:reset)

      gaps = all_issues.take(all_issues.count - 1).zip(all_issues.drop(1)).map do |a, b|
        b.relative_position - a.relative_position
      end

      expect(caching.issue_count).to eq(900)
      expect(gaps).to all(be > RelativePositioning::MIN_GAP)
      expect(all_issues.first.relative_position).to be > (RelativePositioning::MIN_POSITION * 0.9999)
      expect(all_issues.last.relative_position).to be < (RelativePositioning::MAX_POSITION * 0.9999)
      expect(project.root_namespace.issue_repositioning_disabled?).to be false
      expect(project.issues.with_null_relative_position.count).to eq(100)
    end

    it 'is idempotent' do
      expect do
        service.execute
        service.execute
      end.not_to change { issues_in_position_order.map(&:id) }
    end

    it 'acts if the flag is enabled for the root namespace' do
      issue = create(:issue, project: project, author: user, relative_position: max_pos)

      expect { service.execute }.to change { issue.reload.relative_position }
    end

    it 'acts if the flag is enabled for the group' do
      issue = create(:issue, project: project, author: user, relative_position: max_pos)
      project.update!(group: create(:group))

      expect { service.execute }.to change { issue.reload.relative_position }
    end

    it 'aborts if there are too many rebalances running' do
      caching = service.send(:caching)
      allow(caching).to receive(:rebalance_in_progress?).and_return(false)
      allow(caching).to receive(:concurrent_running_rebalances_count).and_return(10)
      allow(service).to receive(:caching).and_return(caching)

      expect { service.execute }.to raise_error(Issues::RelativePositionRebalancingService::TooManyConcurrentRebalances)
      expect(project.root_namespace.issue_repositioning_disabled?).to be false
    end

    it 'resumes a started rebalance even if there are already too many rebalances running' do
      Gitlab::Redis::SharedState.with do |redis|
        redis.sadd("gitlab:issues-position-rebalances:running_rebalances",
          [
            "#{::Gitlab::Issues::Rebalancing::State::PROJECT}/#{project.id}",
            "1/100"
          ]
        )
      end

      caching = service.send(:caching)
      allow(caching).to receive(:concurrent_running_rebalances_count).and_return(10)
      allow(service).to receive(:caching).and_return(caching)

      expect { service.execute }.not_to raise_error
    end

    context 're-balancing is retried on statement timeout exceptions' do
      subject { service }

      it 'retries update statement' do
        call_count = 0
        allow(subject).to receive(:run_update_query) do
          call_count += 1
          if call_count < 13
            raise(ActiveRecord::QueryCanceled)
          else
            call_count = 0 if call_count == 13 + 16 # 16 = 17 sub-batches - 1 call that succeeded as part of 5th batch
            true
          end
        end

        # call math:
        # batches start at 100 and are split in half after every 3 retries if ActiveRecord::StatementTimeout exception is raised.
        # We raise ActiveRecord::StatementTimeout exception for 13 calls:
        # 1. 100 => 3 calls
        # 2. 100/2=50 => 3 calls + 3 above = 6 calls, raise ActiveRecord::StatementTimeout
        # 3. 50/2=25 => 3 calls + 6 above = 9 calls, raise ActiveRecord::StatementTimeout
        # 4. 25/2=12 => 3 calls + 9 above = 12 calls, raise ActiveRecord::StatementTimeout
        # 5. 12/2=6 => 1 call + 12 above = 13 calls, run successfully
        #
        # so out of 100 elements we created batches of 6 items => 100/6 = 17 sub-batches of 6 or less elements
        #
        # project.issues.count: 900 issues, so 9 batches of 100 => 9 * (13+16) = 261
        expect(subject).to receive(:update_positions).exactly(261).times.and_call_original

        subject.execute
      end
    end

    context 'when resuming a stopped rebalance' do
      before do
        service.send(:preload_issue_ids)
        expect(service.send(:caching).get_cached_issue_ids(0, 300)).not_to be_empty
        # simulate we already rebalanced half the issues
        index = clump_size * 3 / 2 + 1
        service.send(:caching).cache_current_index(index)
      end

      it 'rebalances the other half of issues' do
        expect(subject).to receive(:update_positions_with_retry).exactly(5).and_call_original

        subject.execute
      end
    end

    shared_examples 'no-op on the retried job' do
      it 'does not update positions in the 2nd .execute' do
        original_order = issues_in_position_order.map(&:id)

        # preloads issue ids on both runs
        expect(service).to receive(:preload_issue_ids).twice.and_call_original

        # 1st run performs rebalancing
        expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
        expect { service.execute }.to raise_error(StandardError)

        # 2nd run is a no-op
        expect(service).not_to receive(:update_positions_with_retry)
        expect { service.execute }.to raise_error(StandardError)

        # order is preserved
        expect(original_order).to match_array(issues_in_position_order.map(&:id))
      end
    end

    context 'when error is raised in cache cleanup step' do
      let_it_be(:root_namespace_id) { project.root_namespace.id }

      context 'when srem fails' do
        before do
          Gitlab::Redis::SharedState.with do |redis|
            allow(redis).to receive(:srem?).and_raise(StandardError)
          end
        end

        it_behaves_like 'no-op on the retried job'
      end

      context 'when delete issues ids sorted set fails' do
        before do
          Gitlab::Redis::SharedState.with do |redis|
            allow(redis).to receive(:del).and_call_original
            allow(redis).to receive(:del)
              .with("#{Gitlab::Issues::Rebalancing::State::REDIS_KEY_PREFIX}:#{root_namespace_id}")
              .and_raise(StandardError)
          end
        end

        it_behaves_like 'no-op on the retried job'
      end

      context 'when delete current_index_key fails' do
        before do
          Gitlab::Redis::SharedState.with do |redis|
            allow(redis).to receive(:del).and_call_original
            allow(redis).to receive(:del)
              .with("#{Gitlab::Issues::Rebalancing::State::REDIS_KEY_PREFIX}:#{root_namespace_id}:current_index")
              .and_raise(StandardError)
          end
        end

        it_behaves_like 'no-op on the retried job'
      end

      context 'when setting recently finished key fails' do
        before do
          Gitlab::Redis::SharedState.with do |redis|
            allow(redis).to receive(:set).and_call_original
            allow(redis).to receive(:set)
              .with(
                "#{Gitlab::Issues::Rebalancing::State::RECENTLY_FINISHED_REBALANCE_PREFIX}:2:#{project.id}",
                anything,
                anything
              )
              .and_raise(StandardError)
          end
        end

        it 'reruns the next job in full' do
          original_order = issues_in_position_order.map(&:id)

          # preloads issue ids on both runs
          expect(service).to receive(:preload_issue_ids).twice.and_call_original

          # 1st run performs rebalancing
          expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
          expect { service.execute }.to raise_error(StandardError)

          # 2nd run performs rebalancing in full
          expect(service).to receive(:update_positions_with_retry).exactly(9).times.and_call_original
          expect { service.execute }.to raise_error(StandardError)

          # order is preserved
          expect(original_order).to match_array(issues_in_position_order.map(&:id))
        end
      end
    end
  end
end


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

module Issues
  class BaseService < ::IssuableBaseService
    extend ::Gitlab::Utils::Override
    include IncidentManagement::UsageData
    include IssueTypeHelpers

    def hook_data(issue, action, old_associations: {})
      hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
      hook_data[:object_attributes][:action] = action

      hook_data
    end

    def reopen_service
      Issues::ReopenService
    end

    def close_service
      Issues::CloseService
    end

    NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999))

    def rebalance_if_needed(issue)
      return unless issue
      return if issue.relative_position.nil?
      return if NO_REBALANCING_NEEDED.cover?(issue.relative_position)

      Issues::RebalancingWorker.perform_async(nil, *issue.project.self_or_root_group_ids)
    end

    private

    # overriding this because IssuableBaseService#constructor_container_arg returns { project: value }
    # Issues::ReopenService constructor signature is different now, it takes container instead of project also
    # IssuableBaseService#change_state dynamically picks one of the `Issues::ReopenService`, `Epics::ReopenService` or
    # MergeRequests::ReopenService, so we need this method to return { }container: value } for Issues::ReopenService
    def self.constructor_container_arg(value)
      { container: value }
    end

    def find_work_item_type_id(issue_type)
      work_item_type = WorkItems::Type.default_by_type(issue_type)
      work_item_type ||= WorkItems::Type.default_issue_type

      work_item_type.id
    end

    def filter_params(issue)
      super

      params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])

      if params[:work_item_type].present? && !create_issue_type_allowed?(project, params[:work_item_type].base_type)
        params.delete(:work_item_type)
      end

      moved_issue = params.delete(:moved_issue)

      # Setting created_at, updated_at and iid is allowed only for admins and owners or
      # when moving an issue as we preserve the original issue attributes except id and iid.
      params.delete(:iid) unless current_user.can?(:set_issue_iid, project)
      params.delete(:created_at) unless moved_issue || current_user.can?(:set_issue_created_at, project)
      params.delete(:updated_at) unless moved_issue || current_user.can?(:set_issue_updated_at, project)

      # Only users with permission to handle error data can add it to issues
      params.delete(:sentry_issue_attributes) unless current_user.can?(:update_sentry_issue, project)

      issue.system_note_timestamp = params[:created_at] || params[:updated_at]
    end

    override :handle_move_between_ids
    def handle_move_between_ids(issue)
      issue.check_repositioning_allowed! if params[:move_between_ids]

      super

      rebalance_if_needed(issue)
    end

    def handle_escalation_status_change(issue)
      return unless issue.supports_escalation?

      if issue.escalation_status
        ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(
          issue,
          current_user
        ).execute
      else
        ::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute
      end
    end

    def issuable_for_positioning(id, positioning_scope)
      return unless id

      positioning_scope.find(id)
    end

    def create_assignee_note(issue, old_assignees)
      SystemNoteService.change_issuable_assignees(
        issue, issue.project, current_user, old_assignees)
    end

    def execute_hooks(issue, action = 'open', old_associations: {})
      issue_data  = Gitlab::Lazy.new { hook_data(issue, action, old_associations: old_associations) }
      hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
      issue.namespace.execute_hooks(issue_data, hooks_scope)
      issue.namespace.execute_integrations(issue_data, hooks_scope)

      execute_incident_hooks(issue, issue_data) if issue.work_item_type&.incident?
      execute_group_mention_hooks(issue, issue_data) if action == 'open'
    end

    # We can remove this code after proposal in
    # https://gitlab.com/gitlab-org/gitlab/-/issues/367550#proposal is updated.
    def execute_incident_hooks(issue, issue_data)
      issue_data[:object_kind] = 'incident'
      issue_data[:event_type] = 'incident'
      issue.namespace.execute_integrations(issue_data, :incident_hooks)
    end

    def execute_group_mention_hooks(issue, issue_data)
      return unless issue.instance_of?(Issue)

      args = {
        mentionable_type: 'Issue',
        mentionable_id: issue.id,
        hook_data: issue_data,
        is_confidential: issue.confidential?
      }

      issue.run_after_commit_or_now do
        Integrations::GroupMentionWorker.perform_async(args)
      end
    end

    def update_project_counter_caches?(issue)
      super || issue.confidential_changed?
    end

    def log_audit_event(issue, user, event_type, message)
      # defined in EE
    end
  end
end

Issues::BaseService.prepend_mod_with('Issues::BaseService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::BaseService, feature_category: :team_planning do
  describe '#constructor_container_arg' do
    it { expect(described_class.constructor_container_arg("some-value")).to eq({ container: "some-value" }) }
  end
end


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

module Issues
  class MoveService < Issuable::Clone::BaseService
    extend ::Gitlab::Utils::Override

    MoveError = Class.new(StandardError)

    def execute(issue, target_project)
      @target_project = target_project

      verify_can_move_issue!(issue, target_project)

      super

      notify_participants

      # Updates old issue sent notifications allowing
      # to receive service desk emails on the new moved issue.
      update_service_desk_sent_notifications

      copy_email_participants
      queue_copy_designs

      new_entity
    end

    private

    attr_reader :target_project

    def verify_can_move_issue!(issue, target_project)
      unless issue.supports_move_and_clone?
        raise MoveError, s_('MoveIssue|Cannot move issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type }
      end

      unless issue.can_move?(current_user, @target_project)
        raise MoveError, s_('MoveIssue|Cannot move issue due to insufficient permissions!')
      end

      if @project == @target_project
        raise MoveError, s_('MoveIssue|Cannot move issue to project it originates from!')
      end
    end

    def update_service_desk_sent_notifications
      return unless original_entity.from_service_desk?

      original_entity
        .sent_notifications.update_all(project_id: new_entity.project_id, noteable_id: new_entity.id)
    end

    def copy_email_participants
      new_attributes = { id: nil, issue_id: new_entity.id }

      new_participants = original_entity.issue_email_participants.dup

      new_participants.each do |participant|
        participant.assign_attributes(new_attributes)
      end

      IssueEmailParticipant.bulk_insert!(new_participants)
    end

    override :update_old_entity
    def update_old_entity
      super

      rewrite_related_issues
      mark_as_moved
    end

    override :update_new_entity
    def update_new_entity
      super

      copy_contacts
    end

    def create_new_entity
      new_params = {
        id: nil,
        iid: nil,
        relative_position: relative_position,
        project: target_project,
        author: original_entity.author,
        assignee_ids: original_entity.assignee_ids,
        moved_issue: true
      }

      new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params)
      new_params = new_params.merge(rewritten_old_entity_attributes)
      # spam checking is not necessary, as no new content is being created.

      # Skip creation of system notes for existing attributes of the issue. The system notes of the old
      # issue are copied over so we don't want to end up with duplicate notes.
      create_result = CreateService.new(
        container: @target_project,
        current_user: @current_user,
        params: new_params,
        perform_spam_check: false
      ).execute(skip_system_notes: true)

      raise MoveError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank?

      create_result[:issue]
    end

    def queue_copy_designs
      return unless original_entity.designs.present?

      response = DesignManagement::CopyDesignCollection::QueueService.new(
        current_user,
        original_entity,
        new_entity
      ).execute

      log_error(response.message) if response.error?
    end

    def mark_as_moved
      original_entity.update(moved_to: new_entity)
    end

    def rewrite_related_issues
      source_issue_links = IssueLink.for_source(original_entity)
      source_issue_links.update_all(source_id: new_entity.id)

      target_issue_links = IssueLink.for_target(original_entity)
      target_issue_links.update_all(target_id: new_entity.id)
    end

    def copy_contacts
      return unless original_entity.project.root_ancestor == new_entity.project.root_ancestor

      new_entity.customer_relations_contacts = original_entity.customer_relations_contacts
    end

    def notify_participants
      notification_service.async.issue_moved(original_entity, new_entity, @current_user)
    end

    def add_note_from
      SystemNoteService.noteable_moved(
        new_entity,
        target_project,
        original_entity,
        current_user,
        direction: :from
      )
    end

    def add_note_to
      SystemNoteService.noteable_moved(
        original_entity,
        old_project,
        new_entity,
        current_user,
        direction: :to
      )
    end
  end
end

Issues::MoveService.prepend_mod_with('Issues::MoveService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Issues::MoveService, feature_category: :team_planning do
  include DesignManagementTestHelpers

  let_it_be(:user) { create(:user) }
  let_it_be(:author) { create(:user) }
  let_it_be(:title) { 'Some issue' }
  let_it_be(:description) { "Some issue description with mention to #{user.to_reference}" }
  let_it_be(:group) { create(:group, :private) }
  let_it_be(:sub_group_1) { create(:group, :private, parent: group) }
  let_it_be(:sub_group_2) { create(:group, :private, parent: group) }
  let_it_be(:old_project) { create(:project, namespace: sub_group_1) }
  let_it_be(:new_project) { create(:project, namespace: sub_group_2) }

  let(:old_issue) do
    create(:issue, title: title, description: description, project: old_project, author: author, created_at: 1.day.ago, updated_at: 1.day.ago)
  end

  subject(:move_service) do
    described_class.new(container: old_project, current_user: user)
  end

  shared_context 'user can move issue' do
    before do
      old_project.add_reporter(user)
      new_project.add_reporter(user)
    end
  end

  describe '#execute' do
    shared_context 'issue move executed' do
      let!(:new_issue) { move_service.execute(old_issue, new_project) }
    end

    context 'when issue creation fails' do
      include_context 'user can move issue'

      before do
        allow_next_instance_of(Issues::CreateService) do |create_service|
          allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error'))
        end
      end

      it 'raises a move error' do
        expect { move_service.execute(old_issue, new_project) }.to raise_error(
          Issues::MoveService::MoveError,
          'some error'
        )
      end
    end

    context 'issue movable' do
      include_context 'user can move issue'

      it 'creates resource state event' do
        expect { move_service.execute(old_issue, new_project) }.to change(ResourceStateEvent.where(issue_id: old_issue), :count).by(1)
      end

      context 'generic issue' do
        include_context 'issue move executed'

        it 'creates a new issue in a new project' do
          expect(new_issue.project).to eq new_project
          expect(new_issue.namespace_id).to eq new_project.project_namespace_id
        end

        it 'copies issue title' do
          expect(new_issue.title).to eq title
        end

        it 'copies issue description' do
          expect(new_issue.description).to eq description
        end

        it 'adds system note to old issue at the end' do
          expect(old_issue.notes.last.note).to start_with 'moved to'
        end

        it 'adds system note to new issue at the end', :freeze_time do
          system_note = new_issue.notes.last

          expect(system_note.note).to start_with 'moved from'
          expect(system_note.created_at).to be_like_time(Time.current)
        end

        it 'closes old issue' do
          expect(old_issue.closed?).to be true
        end

        it 'persists new issue' do
          expect(new_issue.persisted?).to be true
        end

        it 'persists all changes' do
          expect(old_issue.changed?).to be false
          expect(new_issue.changed?).to be false
        end

        it 'preserves author' do
          expect(new_issue.author).to eq author
        end

        it 'creates a new internal id for issue' do
          expect(new_issue.iid).to be 1
        end

        it 'marks issue as moved' do
          expect(old_issue.moved?).to eq true
          expect(old_issue.moved_to).to eq new_issue
        end

        it 'marks issue as closed' do
          expect(old_issue.closed?).to eq true
        end

        it 'preserves create time' do
          expect(old_issue.created_at).to eq new_issue.created_at
        end
      end

      context 'issue with award emoji' do
        let!(:award_emoji) { create(:award_emoji, awardable: old_issue) }

        it 'copies the award emoji' do
          old_issue.reload
          new_issue = move_service.execute(old_issue, new_project)

          expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name
        end
      end

      context 'issue with milestone' do
        let(:milestone) { create(:milestone, group: sub_group_1) }
        let(:new_project) { create(:project, namespace: sub_group_1) }

        let(:old_issue) do
          create(:issue, title: title, description: description, project: old_project, author: author, milestone: milestone)
        end

        before do
          create(:resource_milestone_event, issue: old_issue, milestone: milestone, action: :add)
        end

        it 'does not create extra milestone events' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.resource_milestone_events.count).to eq(old_issue.resource_milestone_events.count)
        end
      end

      context 'issue with due date' do
        let(:old_issue) do
          create(:issue, title: title, description: description, project: old_project, author: author, due_date: '2020-01-10')
        end

        before do
          old_issue.update!(due_date: Date.today)
          SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date'))
        end

        it 'does not create extra system notes' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.notes.count).to eq(old_issue.notes.count)
        end
      end

      context 'issue with assignee' do
        let_it_be(:assignee) { create(:user) }

        before do
          old_issue.assignees = [assignee]
        end

        it 'preserves assignee with access to the new issue' do
          new_project.add_reporter(assignee)

          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.assignees).to eq([assignee])
        end

        it 'ignores assignee without access to the new issue' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.assignees).to be_empty
        end
      end

      context 'issue with contacts' do
        let_it_be(:contacts) { create_list(:contact, 2, group: group) }

        before do
          old_issue.customer_relations_contacts = contacts
        end

        it 'preserves contacts' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.customer_relations_contacts).to eq(contacts)
        end

        context 'when moving to another root group' do
          let(:another_project) { create(:project, namespace: create(:group)) }

          before do
            another_project.add_reporter(user)
          end

          it 'does not preserve contacts' do
            new_issue = move_service.execute(old_issue, another_project)

            expect(new_issue.customer_relations_contacts).to be_empty
          end
        end
      end

      context 'moving to same project' do
        let(:new_project) { old_project }

        it 'raises error' do
          expect { move_service.execute(old_issue, new_project) }
            .to raise_error(StandardError, /Cannot move issue/)
        end
      end

      context 'project issue hooks' do
        let_it_be(:old_project_hook) { create(:project_hook, project: old_project, issues_events: true) }
        let_it_be(:new_project_hook) { create(:project_hook, project: new_project, issues_events: true) }

        let(:expected_new_project_hook_payload) do
          hash_including(
            event_type: 'issue',
            object_kind: 'issue',
            object_attributes: include(
              project_id: new_project.id,
              state: 'opened',
              action: 'open'
            )
          )
        end

        let(:expected_old_project_hook_payload) do
          hash_including(
            event_type: 'issue',
            object_kind: 'issue',
            changes: {
              state_id: { current: 2, previous: 1 },
              closed_at: { current: kind_of(Time), previous: nil },
              updated_at: { current: kind_of(Time), previous: kind_of(Time) }
            },
            object_attributes: include(
              id: old_issue.id,
              closed_at: kind_of(Time),
              state: 'closed',
              action: 'close'
            )
          )
        end

        it 'executes project issue hooks for both projects' do
          expect_next_instance_of(WebHookService, new_project_hook, expected_new_project_hook_payload, 'issue_hooks') do |service|
            expect(service).to receive(:async_execute).once
          end
          expect_next_instance_of(WebHookService, old_project_hook, expected_old_project_hook_payload, 'issue_hooks') do |service|
            expect(service).to receive(:async_execute).once
          end

          move_service.execute(old_issue, new_project)
        end
      end

      # These tests verify that notes are copied. More thorough tests are in
      # the unit test for Notes::CopyService.
      context 'issue with notes' do
        let!(:notes) do
          [
            create(:note, noteable: old_issue, project: old_project, created_at: 2.weeks.ago, updated_at: 1.week.ago),
            create(:note, noteable: old_issue, project: old_project)
          ]
        end

        let(:copied_notes) { new_issue.notes.limit(notes.size) } # Remove the system note added by the copy itself

        include_context 'issue move executed'

        it 'copies existing notes in order' do
          expect(copied_notes.order('id ASC').pluck(:note)).to eq(notes.map(&:note))
        end
      end

      context 'issue with a design', :clean_gitlab_redis_shared_state do
        let_it_be(:new_project) { create(:project) }

        let!(:design) { create(:design, :with_lfs_file, issue: old_issue) }
        let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) }
        let(:subject) { move_service.execute(old_issue, new_project) }

        before do
          enable_design_management
        end

        it 'calls CopyDesignCollection::QueueService' do
          expect(DesignManagement::CopyDesignCollection::QueueService).to receive(:new)
            .with(user, old_issue, kind_of(Issue))
            .and_call_original

          subject
        end

        it 'logs if QueueService returns an error', :aggregate_failures do
          error_message = 'error'

          expect_next_instance_of(DesignManagement::CopyDesignCollection::QueueService) do |service|
            expect(service).to receive(:execute).and_return(
              ServiceResponse.error(message: error_message)
            )
          end
          expect(Gitlab::AppLogger).to receive(:error).with(error_message)

          subject
        end

        # Perform a small integration test to ensure the services and worker
        # can correctly create designs.
        it 'copies the design and its notes', :sidekiq_inline, :aggregate_failures do
          new_issue = subject

          expect(new_issue.designs.size).to eq(1)
          expect(new_issue.designs.first.notes.size).to eq(1)
        end
      end

      context 'issue relative position' do
        let(:subject) { move_service.execute(old_issue, new_project) }

        it_behaves_like 'copy or reset relative position'
      end

      context 'issue with escalation status' do
        it 'keeps the escalation status' do
          escalation_status = create(:incident_management_issuable_escalation_status, issue: old_issue)

          move_service.execute(old_issue, new_project)

          expect(escalation_status.reload.issue).to eq(old_issue)
        end
      end
    end

    describe 'move permissions' do
      let(:move) { move_service.execute(old_issue, new_project) }

      context 'user is reporter in both projects' do
        include_context 'user can move issue'
        it { expect { move }.not_to raise_error }
      end

      context 'user is reporter only in new project' do
        before do
          new_project.add_reporter(user)
        end

        it { expect { move }.to raise_error(StandardError, /permissions/) }
      end

      context 'user is reporter only in old project' do
        before do
          old_project.add_reporter(user)
        end

        it { expect { move }.to raise_error(StandardError, /permissions/) }
      end

      context 'user is reporter in one project and guest in another' do
        before do
          new_project.add_guest(user)
          old_project.add_reporter(user)
        end

        it { expect { move }.to raise_error(StandardError, /permissions/) }
      end

      context 'issue has already been moved' do
        include_context 'user can move issue'

        let(:moved_to_issue) { create(:issue) }

        let(:old_issue) do
          create(:issue, project: old_project, author: author, moved_to: moved_to_issue)
        end

        it { expect { move }.to raise_error(StandardError, /permissions/) }
      end

      context 'issue is not persisted' do
        include_context 'user can move issue'
        let(:old_issue) { build(:issue, project: old_project, author: author) }

        it { expect { move }.to raise_error(StandardError, /permissions/) }
      end
    end
  end

  describe '#rewrite_related_issues' do
    include_context 'user can move issue'

    let(:admin) { create(:admin) }
    let(:authorized_project) { create(:project) }
    let(:authorized_project2) { create(:project) }
    let(:unauthorized_project) { create(:project) }

    let(:authorized_issue_b) { create(:issue, project: authorized_project) }
    let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
    let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
    let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }

    let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
    let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
    let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
    let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }

    before do
      authorized_project.add_developer(user)
      authorized_project.add_developer(admin)
      authorized_project2.add_developer(user)
      authorized_project2.add_developer(admin)
    end

    context 'multiple related issues' do
      context 'when admin mode is enabled', :enable_admin_mode do
        it 'moves all related issues and retains permissions' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.related_issues(admin))
            .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])

          expect(new_issue.related_issues(user))
            .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])

          expect(authorized_issue_d.related_issues(user))
            .to match_array([new_issue])
        end
      end

      context 'when admin mode is disabled' do
        it 'moves all related issues and retains permissions' do
          new_issue = move_service.execute(old_issue, new_project)

          expect(new_issue.related_issues(admin))
              .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])

          expect(new_issue.related_issues(user))
              .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])

          expect(authorized_issue_d.related_issues(user))
              .to match_array([new_issue])
        end
      end
    end
  end

  context 'updating sent notifications' do
    let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
    let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
    let!(:other_issue_notification) { create(:sent_notification, project: old_issue.project) }

    include_context 'user can move issue'

    context 'when issue is from service desk' do
      before do
        allow(old_issue).to receive(:from_service_desk?).and_return(true)
      end

      it 'updates moved issue sent notifications' do
        new_issue = move_service.execute(old_issue, new_project)

        old_issue_notification_1.reload
        old_issue_notification_2.reload
        expect(old_issue_notification_1.project_id).to eq(new_issue.project_id)
        expect(old_issue_notification_1.noteable_id).to eq(new_issue.id)
        expect(old_issue_notification_2.project_id).to eq(new_issue.project_id)
        expect(old_issue_notification_2.noteable_id).to eq(new_issue.id)
      end

      it 'does not update other issues sent notifications' do
        expect do
          move_service.execute(old_issue, new_project)
          other_issue_notification.reload
        end.not_to change { other_issue_notification.noteable_id }
      end
    end

    context 'when issue is not from service desk' do
      it 'does not update sent notifications' do
        move_service.execute(old_issue, new_project)

        old_issue_notification_1.reload
        old_issue_notification_2.reload
        expect(old_issue_notification_1.project_id).to eq(old_issue.project_id)
        expect(old_issue_notification_1.noteable_id).to eq(old_issue.id)
        expect(old_issue_notification_2.project_id).to eq(old_issue.project_id)
        expect(old_issue_notification_2.noteable_id).to eq(old_issue.id)
      end
    end
  end

  context 'copying email participants' do
    let!(:participant1) { create(:issue_email_participant, email: 'user1@example.com', issue: old_issue) }
    let!(:participant2) { create(:issue_email_participant, email: 'user2@example.com', issue: old_issue) }
    let!(:participant3) { create(:issue_email_participant, email: 'other_project_customer@example.com') }

    include_context 'user can move issue'

    subject(:new_issue) do
      move_service.execute(old_issue, new_project)
    end

    it 'copies moved issue email participants' do
      new_issue

      expect(participant1.reload.issue).to eq(old_issue)
      expect(participant2.reload.issue).to eq(old_issue)
      expect(new_issue.issue_email_participants.pluck(:email))
       .to match_array([participant1.email, participant2.email])
    end
  end
end