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

module ImportCsv
  class PreprocessMilestonesService < BaseService
    def initialize(user, project, provided_titles)
      @user = user
      @project = project
      @provided_titles = provided_titles

      @results = { success: 0, errors: nil }
      @milestone_errors = { missing: { header: {}, titles: [] } }
    end

    attr_reader :user, :project, :provided_titles, :results, :milestone_errors

    def execute
      result = find_milestones_by_titles
      available_milestones = result.map(&:title).uniq.sort
      return ServiceResponse.success(payload: result) if provided_titles.sort == available_milestones

      milestone_errors[:missing][:header] = 'Milestone'
      milestone_errors[:missing][:titles] = provided_titles.difference(available_milestones) || []
      ServiceResponse.error(message: "", payload: milestone_errors)
    end

    def find_milestones_by_titles
      # Find if these milestones exist in the project or its group and group ancestors
      finder_params = {
        project_ids: [project.id],
        title: provided_titles
      }
      finder_params[:group_ids] = project.group.self_and_ancestors.select(:id) if project.group
      MilestonesFinder.new(finder_params).execute
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ImportCsv::PreprocessMilestonesService, feature_category: :importers do
  let_it_be(:group) { create(:group) }
  let_it_be(:project) { create(:project, group: group) }
  let_it_be(:user) { create(:user) }
  let_it_be(:provided_titles) { %w[15.10 10.1] }

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

  subject { service.execute }

  describe '#execute' do
    let(:project_milestones) { ::MilestonesFinder.new({ project_ids: [project.id] }).execute }

    shared_examples 'csv import' do |is_success:, milestone_errors:|
      it 'does not create milestones' do
        expect { subject }.not_to change { project_milestones.count }
      end

      it 'reports any missing milestones' do
        result = subject

        if is_success
          expect(result).to be_success
        else
          expect(result[:status]).to eq(:error)
          expect(result.payload).to match(milestone_errors)
        end
      end
    end

    context 'with csv that has missing or unavailable milestones' do
      it_behaves_like 'csv import',
        { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: %w[15.10 10.1] } } }
    end

    context 'with csv that includes project milestones' do
      let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }

      it_behaves_like 'csv import',
        { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
    end

    context 'with csv that includes milestones column' do
      let!(:project_milestone) { create(:milestone, project: project, title: '15.10') }

      context 'when milestones exist in the importing projects group' do
        let!(:group_milestone) { create(:milestone, group: group, title: '10.1') }

        it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
      end

      context 'when milestones exist in a subgroup of the importing projects group' do
        let_it_be(:subgroup) { create(:group, parent: group) }
        let!(:group_milestone) { create(:milestone, group: subgroup, title: '10.1') }

        it_behaves_like 'csv import',
          { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
      end

      context 'when milestones exist in a different project from the importing project' do
        let_it_be(:second_project) { create(:project, group: group) }
        let!(:second_project_milestone) { create(:milestone, project: second_project, title: '10.1') }

        it_behaves_like 'csv import',
          { is_success: false, milestone_errors: { missing: { header: 'Milestone', titles: ["10.1"] } } }
      end

      context 'when duplicate milestones exist in the projects group and parent group' do
        let_it_be(:sub_group) { create(:group, parent: group) }
        let_it_be(:project) { create(:project, group: sub_group) }
        let!(:ancestor_group_milestone) { create(:milestone, group: group, title: '15.10') }
        let!(:ancestor_group_milestone_two) { create(:milestone, group: group, title: '10.1') }
        let!(:group_milestone) { create(:milestone, group: sub_group, title: '10.1') }

        it_behaves_like 'csv import', { is_success: true, milestone_errors: nil }
      end
    end
  end
end


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

module AutoMerge
  class MergeWhenPipelineSucceedsService < AutoMerge::BaseService
    def execute(merge_request)
      super do
        add_system_note(merge_request)
      end
    end

    def process(merge_request)
      logger.info("Processing Automerge")
      return unless merge_request.actual_head_pipeline_success?

      logger.info("Pipeline Success")
      return unless merge_request.mergeable?

      logger.info("Merge request mergeable")

      merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
    end

    def cancel(merge_request)
      super do
        SystemNoteService.cancel_merge_when_pipeline_succeeds(merge_request, project, current_user)
      end
    end

    def abort(merge_request, reason)
      super do
        SystemNoteService.abort_merge_when_pipeline_succeeds(merge_request, project, current_user, reason)
      end
    end

    def available_for?(merge_request)
      super do
        check_availability(merge_request)
      end
    end

    private

    def add_system_note(merge_request)
      SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.actual_head_pipeline.sha) if merge_request.saved_change_to_auto_merge_enabled?
    end

    def check_availability(merge_request)
      merge_request.actual_head_pipeline&.active?
    end

    def notify(merge_request)
      notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled?
    end

    def logger
      @logger ||= Gitlab::AppLogger
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AutoMerge::MergeWhenPipelineSucceedsService, feature_category: :code_review_workflow do
  include_context 'for auto_merge strategy context'

  describe "#available_for?" do
    subject { service.available_for?(mr_merge_if_green_enabled) }

    let(:pipeline_status) { :running }

    before do
      create(
        :ci_pipeline,
        pipeline_status,
        ref: mr_merge_if_green_enabled.source_branch,
        sha: mr_merge_if_green_enabled.diff_head_sha,
        project: mr_merge_if_green_enabled.source_project
      )
      mr_merge_if_green_enabled.update_head_pipeline
    end

    it { is_expected.to be_truthy }

    it 'memoizes the result' do
      expect(mr_merge_if_green_enabled).to receive(:can_be_merged_by?).once.and_call_original

      2.times { is_expected.to be_truthy }
    end

    context 'when the head pipeline succeeded' do
      let(:pipeline_status) { :success }

      it { is_expected.to be_falsy }
    end

    context 'when the user does not have permission to merge' do
      before do
        allow(mr_merge_if_green_enabled).to receive(:can_be_merged_by?) { false }
      end

      it { is_expected.to be_falsy }
    end
  end

  describe "#execute" do
    it_behaves_like 'auto_merge service #execute', 'merge_when_pipeline_succeeds' do
      let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
      let(:expected_note) do
        "enabled an automatic merge when the pipeline for #{pipeline.sha}"
      end
    end
  end

  describe "#process" do
    it_behaves_like 'auto_merge service #process'
  end

  describe '#cancel' do
    it_behaves_like 'auto_merge service #cancel'
  end

  describe '#abort' do
    it_behaves_like 'auto_merge service #abort'
  end

  describe 'pipeline integration' do
    context 'when there are multiple stages in the pipeline' do
      let(:ref) { mr_merge_if_green_enabled.source_branch }
      let(:sha) { project.commit(ref).id }

      let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }

      let(:pipeline) do
        create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
      end

      let!(:build) do
        create(
          :ci_build,
          :created,
          pipeline: pipeline,
          ref: ref,
          name: 'build',
          ci_stage: build_stage
        )
      end

      let!(:test) do
        create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test')
      end

      before do
        # This behavior of MergeRequest: we instantiate a new object
        #
        allow_any_instance_of(MergeRequest)
          .to receive(:head_pipeline)
          .and_wrap_original do
            Ci::Pipeline.find(pipeline.id)
          end
      end

      it "doesn't merge if any of stages failed" do
        expect(MergeWorker).not_to receive(:perform_async)

        build.success
        test.reload
        test.drop
      end

      it 'merges when all stages succeeded', :sidekiq_might_not_need_inline do
        expect(MergeWorker).to receive(:perform_async)

        build.success
        test.reload
        test.success
      end
    end
  end
end


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

module AutoMerge
  class BaseService < ::BaseService
    include Gitlab::Utils::StrongMemoize
    include MergeRequests::AssignsMergeParams

    def execute(merge_request)
      ApplicationRecord.transaction do
        register_auto_merge_parameters!(merge_request)
        yield if block_given?
      end

      notify(merge_request)
      AutoMergeProcessWorker.perform_async(merge_request.id)

      strategy.to_sym
    rescue StandardError => e
      track_exception(e, merge_request)
      :failed
    end

    def process(_)
      raise NotImplementedError
    end

    def update(merge_request)
      assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))

      return :failed unless merge_request.save

      strategy.to_sym
    end

    def cancel(merge_request)
      ApplicationRecord.transaction do
        clear_auto_merge_parameters!(merge_request)
        yield if block_given?
      end

      success
    rescue StandardError => e
      track_exception(e, merge_request)
      error("Can't cancel the automatic merge", 406)
    end

    def abort(merge_request, reason)
      ApplicationRecord.transaction do
        clear_auto_merge_parameters!(merge_request)
        yield if block_given?
      end

      success
    rescue StandardError => e
      track_exception(e, merge_request)
      error("Can't abort the automatic merge", 406)
    end

    def available_for?(merge_request)
      strong_memoize("available_for_#{merge_request.id}") do
        merge_request.can_be_merged_by?(current_user) &&
          merge_request.open? &&
          !merge_request.broken? &&
          overrideable_available_for_checks(merge_request) &&
          yield
      end
    end

    private

    def overrideable_available_for_checks(merge_request)
      !merge_request.draft? &&
        merge_request.mergeable_discussions_state? &&
        !merge_request.merge_blocked_by_other_mrs?
    end

    # Overridden in child classes
    def notify(merge_request)
    end

    def strategy
      strong_memoize(:strategy) do
        self.class.name.demodulize.remove('Service').underscore
      end
    end

    def register_auto_merge_parameters!(merge_request)
      assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
      merge_request.auto_merge_enabled = true
      merge_request.merge_user = current_user
      merge_request.save!
    end

    def clear_auto_merge_parameters!(merge_request)
      merge_request.auto_merge_enabled = false
      merge_request.merge_user = nil

      merge_request.merge_params&.except!(*clearable_auto_merge_parameters)

      merge_request.save!
    end

    # Overridden in EE child classes
    def clearable_auto_merge_parameters
      %w[
        should_remove_source_branch
        commit_message
        squash_commit_message
        auto_merge_strategy
      ]
    end

    def track_exception(error, merge_request)
      Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AutoMerge::BaseService, feature_category: :code_review_workflow do
  let(:project) { create(:project) }
  let(:user) { create(:user) }
  let(:service) { described_class.new(project, user, params) }
  let(:merge_request) { create(:merge_request) }
  let(:params) { {} }

  describe '#execute' do
    subject { service.execute(merge_request) }

    before do
      allow(AutoMergeProcessWorker).to receive(:perform_async) {}
    end

    it 'sets properies to the merge request' do
      subject

      merge_request.reload
      expect(merge_request).to be_auto_merge_enabled
      expect(merge_request.merge_user).to eq(user)
      expect(merge_request.auto_merge_strategy).to eq('base')
    end

    it 'yields block' do
      expect { |b| service.execute(merge_request, &b) }.to yield_control.once
    end

    it 'returns activated strategy name' do
      is_expected.to eq(:base)
    end

    context 'when merge parameters are given' do
      let(:params) do
        {
          'commit_message' => "Merge branch 'patch-12' into 'master'",
          'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18",
          'should_remove_source_branch' => false,
          'squash' => false,
          'squash_commit_message' => "Update README.md"
        }
      end

      it 'sets merge parameters' do
        subject

        merge_request.reload
        expect(merge_request.merge_params['commit_message']).to eq("Merge branch 'patch-12' into 'master'")
        expect(merge_request.merge_params['sha']).to eq('200fcc9c260f7219eaf0daba87d818f0922c5b18')
        expect(merge_request.merge_params['should_remove_source_branch']).to eq(false)
        expect(merge_request.squash_on_merge?).to eq(false)
        expect(merge_request.merge_params['squash_commit_message']).to eq('Update README.md')
      end
    end

    context 'when strategy is merge when pipeline succeeds' do
      let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) }

      before do
        pipeline = build(:ci_pipeline)
        allow(merge_request).to receive(:actual_head_pipeline) { pipeline }
      end

      it 'sets the auto merge strategy' do
        subject

        merge_request.reload
        expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
      end

      it 'returns activated strategy name' do
        is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS.to_sym)
      end

      it 'calls AutoMergeProcessWorker' do
        expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id).once

        subject
      end
    end

    context 'when failed to save merge request' do
      before do
        allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid }
      end

      it 'does not yield block' do
        expect { |b| service.execute(merge_request, &b) }.not_to yield_control
      end

      it 'returns failed' do
        is_expected.to eq(:failed)
      end

      it 'tracks the exception' do
        expect(Gitlab::ErrorTracking)
          .to receive(:track_exception)
          .with(kind_of(ActiveRecord::RecordInvalid), merge_request_id: merge_request.id)

        subject
      end
    end

    context 'when exception happens in yield block' do
      def execute_with_error_in_yield
        service.execute(merge_request) { raise 'Something went wrong' }
      end

      it 'returns failed status' do
        expect(execute_with_error_in_yield).to eq(:failed)
      end

      it 'rollback the transaction' do
        execute_with_error_in_yield

        merge_request.reload
        expect(merge_request).not_to be_auto_merge_enabled
      end

      it 'tracks the exception' do
        expect(Gitlab::ErrorTracking)
          .to receive(:track_exception)
          .with(kind_of(RuntimeError), merge_request_id: merge_request.id)

        execute_with_error_in_yield
      end
    end
  end

  describe '#update' do
    subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

    context 'when merge params are specified' do
      let(:params) do
        {
          'commit_message' => "Merge branch 'patch-12' into 'master'",
          'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18",
          'should_remove_source_branch' => false,
          'squash_commit_message' => "Update README.md"
        }
      end

      it 'updates merge params' do
        expect { subject }.to change {
          merge_request.reload.merge_params.slice(*params.keys)
        }.from({}).to(params)
      end
    end
  end

  shared_examples_for 'Canceled or Dropped' do
    it 'removes properies from the merge request' do
      subject

      merge_request.reload
      expect(merge_request).not_to be_auto_merge_enabled
      expect(merge_request.merge_user).to be_nil
      expect(merge_request.auto_merge_strategy).to be_nil
    end

    it 'yields block' do
      expect { |b| service.cancel(merge_request, &b) }.to yield_control.once
    end

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

    context 'when merge params are set' do
      before do
        merge_request.update!(merge_params:
          {
            'should_remove_source_branch' => false,
            'commit_message' => "Merge branch 'patch-12' into 'master'",
            'squash_commit_message' => "Update README.md",
            'auto_merge_strategy' => 'merge_when_pipeline_succeeds'
          })
      end

      it 'removes merge parameters' do
        subject

        merge_request.reload
        expect(merge_request.merge_params['should_remove_source_branch']).to be_nil
        expect(merge_request.merge_params['commit_message']).to be_nil
        expect(merge_request.merge_params['squash_commit_message']).to be_nil
        expect(merge_request.merge_params['auto_merge_strategy']).to be_nil
      end
    end

    context 'when failed to save' do
      before do
        allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid }
      end

      it 'does not yield block' do
        expect { |b| service.execute(merge_request, &b) }.not_to yield_control
      end
    end
  end

  describe '#cancel' do
    subject { service.cancel(merge_request) }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

    it_behaves_like 'Canceled or Dropped'

    context 'when failed to save merge request' do
      before do
        allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid }
      end

      it 'returns error status' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:message]).to eq("Can't cancel the automatic merge")
      end
    end

    context 'when exception happens in yield block' do
      def cancel_with_error_in_yield
        service.cancel(merge_request) { raise 'Something went wrong' }
      end

      it 'returns error' do
        result = cancel_with_error_in_yield
        expect(result[:status]).to eq(:error)
        expect(result[:message]).to eq("Can't cancel the automatic merge")
      end

      it 'rollback the transaction' do
        cancel_with_error_in_yield

        merge_request.reload
        expect(merge_request).to be_auto_merge_enabled
      end

      it 'tracks the exception' do
        expect(Gitlab::ErrorTracking)
          .to receive(:track_exception)
          .with(kind_of(RuntimeError), merge_request_id: merge_request.id)

        cancel_with_error_in_yield
      end
    end
  end

  describe '#abort' do
    subject { service.abort(merge_request, reason) }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
    let(:reason) { 'an error' }

    it_behaves_like 'Canceled or Dropped'

    context 'when failed to save' do
      before do
        allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid }
      end

      it 'returns error status' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:message]).to eq("Can't abort the automatic merge")
      end
    end

    context 'when exception happens in yield block' do
      def abort_with_error_in_yield
        service.abort(merge_request, reason) { raise 'Something went wrong' }
      end

      it 'returns error' do
        result = abort_with_error_in_yield
        expect(result[:status]).to eq(:error)
        expect(result[:message]).to eq("Can't abort the automatic merge")
      end

      it 'rollback the transaction' do
        abort_with_error_in_yield

        merge_request.reload
        expect(merge_request).to be_auto_merge_enabled
      end

      it 'tracks the exception' do
        expect(Gitlab::ErrorTracking)
          .to receive(:track_exception)
          .with(kind_of(RuntimeError), merge_request_id: merge_request.id)

        abort_with_error_in_yield
      end
    end
  end

  describe '#process' do
    specify { expect(service).to respond_to :process }
    specify { expect { service.process(nil) }.to raise_error NotImplementedError }
  end

  describe '#available_for?' do
    using RSpec::Parameterized::TableSyntax

    subject(:available_for) { service.available_for?(merge_request) { true } }

    let(:merge_request) { create(:merge_request) }

    where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :result) do
      true | true | false | true | false | false | true
      false | true | false | true | false | false | false
      true | false | false | true | false | false | false
      true | true | true | true | false | false | false
      true | true | false | false | false | false | false
      true | true | false | true | true | false | false
      true | true | false | true | false | true | false
    end

    with_them do
      before do
        allow(merge_request).to receive(:can_be_merged_by?).and_return(can_be_merged)
        allow(merge_request).to receive(:open?).and_return(open)
        allow(merge_request).to receive(:broken?).and_return(broken)
        allow(merge_request).to receive(:draft?).and_return(draft)
        allow(merge_request).to receive(:mergeable_discussions_state?).and_return(discussions)
        allow(merge_request).to receive(:merge_blocked_by_other_mrs?).and_return(blocked)
      end

      it 'returns the expected results' do
        expect(available_for).to eq(result)
      end
    end
  end
end


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

module AuthorizedProjectUpdate
  class ProjectAccessChangedService
    def initialize(project_ids)
      @project_ids = Array.wrap(project_ids)
    end

    def execute
      return if @project_ids.empty?

      bulk_args = @project_ids.map { |id| [id] }

      AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService, feature_category: :groups_and_projects do
  describe '#execute' do
    it 'executes projects_authorizations refresh' do
      expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
        .with([[1], [2]])

      described_class.new([1, 2]).execute
    end
  end
end


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

module AuthorizedProjectUpdate
  class ProjectRecalculatePerUserService < ProjectRecalculateService
    def initialize(project, user)
      @project = project
      @user = user
    end

    private

    attr_reader :user

    def apply_scopes(project_authorizations)
      super.where(user_id: user.id) # rubocop: disable CodeReuse/ActiveRecord
    end

    def effective_access_levels
      Projects::Members::EffectiveAccessLevelPerUserFinder.new(project, user).execute
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AuthorizedProjectUpdate::ProjectRecalculatePerUserService, '#execute', feature_category: :groups_and_projects do
  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }
  let_it_be(:another_user) { create(:user) }

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

  it 'returns success' do
    expect(execute.success?).to eq(true)
  end

  context 'when there are no changes to be made' do
    it 'does not change authorizations' do
      expect { execute }.not_to(change { ProjectAuthorization.count })
    end
  end

  context 'when there are changes to be made' do
    context 'when addition is required' do
      before do
        project.add_developer(user)
        project.add_developer(another_user)
        project.project_authorizations.where(user: [user, another_user]).delete_all
      end

      it 'adds a new authorization record for the specific user' do
        expect { execute }.to(
          change { project.project_authorizations.where(user: user).count }
          .from(0).to(1)
        )
      end

      it 'does not add a new authorization record for the other user' do
        expect { execute }.not_to(
          change { project.project_authorizations.where(user: another_user).count }
        )
      end

      it 'adds a new authorization record with the correct access level for the specific user' do
        execute

        project_authorization = project.project_authorizations.where(
          user: user,
          access_level: Gitlab::Access::DEVELOPER
        )

        expect(project_authorization).to exist
      end
    end

    context 'when removal is required' do
      before do
        create(:project_authorization, user: user, project: project)
        create(:project_authorization, user: another_user, project: project)
      end

      it 'removes the authorization record for the specific user' do
        expect { execute }.to(
          change { project.project_authorizations.where(user: user).count }
          .from(1).to(0)
        )
      end

      it 'does not remove the authorization record for the other user' do
        expect { execute }.not_to(
          change { project.project_authorizations.where(user: another_user).count }
        )
      end
    end

    context 'when an update in access level is required' do
      before do
        project.add_developer(user)
        project.add_developer(another_user)
        project.project_authorizations.where(user: [user, another_user]).delete_all
        create(:project_authorization, project: project, user: user, access_level: Gitlab::Access::GUEST)
        create(:project_authorization, project: project, user: another_user, access_level: Gitlab::Access::GUEST)
      end

      it 'updates the authorization of the specific user to the correct access level' do
        expect { execute }.to(
          change { project.project_authorizations.find_by(user: user).access_level }
            .from(Gitlab::Access::GUEST).to(Gitlab::Access::DEVELOPER)
        )
      end

      it 'does not update the authorization of the other user to the correct access level' do
        expect { execute }.not_to(
          change { project.project_authorizations.find_by(user: another_user).access_level }
            .from(Gitlab::Access::GUEST)
        )
      end
    end
  end
end


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

module AuthorizedProjectUpdate
  class PeriodicRecalculateService
    BATCH_SIZE = 450
    DELAY_INTERVAL = 50.seconds.to_i

    def execute
      # Using this approach (instead of eg. User.each_batch) keeps the arguments
      # the same for AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker
      # even if the user list changes, so we can deduplicate these jobs.

      # Since UserRefreshOverUserRangeWorker has set data_consistency to delayed,
      # a job enqueued without a delay could fail because the replica could not catch up with the primary.
      # To prevent this, we start the index from `1` instead of `0` so as to ensure that
      # no UserRefreshOverUserRangeWorker job is enqueued without a delay.
      (1..User.maximum(:id)).each_slice(BATCH_SIZE).with_index(1) do |batch, index|
        delay = DELAY_INTERVAL * index
        AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker.perform_in(delay, *batch.minmax)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AuthorizedProjectUpdate::PeriodicRecalculateService, feature_category: :groups_and_projects do
  subject(:service) { described_class.new }

  describe '#execute' do
    let(:batch_size) { 2 }

    let_it_be(:users) { create_list(:user, 4) }

    before do
      stub_const('AuthorizedProjectUpdate::PeriodicRecalculateService::BATCH_SIZE', batch_size)

      User.delete([users[1], users[2]])
    end

    it 'calls AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' do
      (1..User.maximum(:id)).each_slice(batch_size).with_index(1) do |batch, index|
        delay = AuthorizedProjectUpdate::PeriodicRecalculateService::DELAY_INTERVAL * index

        expect(AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker).to(
          receive(:perform_in).with(delay, *batch.minmax))
      end

      service.execute
    end
  end
end


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

module AuthorizedProjectUpdate
  # Service for finding the authorized_projects records of a user that needs addition or removal.
  #
  # Usage:
  #
  #     user = User.find_by(username: 'alice')
  #     service = AuthorizedProjectUpdate::FindRecordsDueForRefreshService.new(some_user)
  #     service.execute
  class FindRecordsDueForRefreshService
    def initialize(user, source: nil, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
      @user = user
      @source = source
      @incorrect_auth_found_callback = incorrect_auth_found_callback
      @missing_auth_found_callback = missing_auth_found_callback
    end

    def execute
      current = current_authorizations_per_project
      fresh = fresh_access_levels_per_project

      # Projects that have more than one authorizations associated with
      # the user needs to be deleted.
      # The correct authorization is added to the ``add`` array in the
      # next stage.
      remove = projects_with_duplicates
      current.except!(*projects_with_duplicates)

      remove |= current.each_with_object([]) do |(project_id, row), array|
        next if fresh[project_id] && fresh[project_id] == row.access_level

        # rows not in the new list or with a different access level should be
        # removed.

        if incorrect_auth_found_callback
          incorrect_auth_found_callback.call(project_id, row.access_level)
        end

        array << row.project_id
      end

      add = fresh.each_with_object([]) do |(project_id, level), array|
        next if current[project_id] && current[project_id].access_level == level

        # rows not in the old list or with a different access level should be
        # added.

        if missing_auth_found_callback
          missing_auth_found_callback.call(project_id, level)
        end

        array << {
          user_id: user.id,
          project_id: project_id,
          access_level: level
        }
      end

      [remove, add]
    end

    def needs_refresh?
      remove, add = execute

      remove.present? || add.present?
    end

    def fresh_access_levels_per_project
      fresh_authorizations.each_with_object({}) do |row, hash|
        hash[row.project_id] = row.access_level
      end
    end

    def current_authorizations_per_project
      current_authorizations.index_by(&:project_id)
    end

    def current_authorizations
      @current_authorizations ||= user.project_authorizations.select(:project_id, :access_level)
    end

    def fresh_authorizations
      Gitlab::ProjectAuthorizations.new(user).calculate
    end

    private

    attr_reader :user, :source, :incorrect_auth_found_callback, :missing_auth_found_callback

    def projects_with_duplicates
      @projects_with_duplicates ||= current_authorizations
                                      .group_by(&:project_id)
                                      .select { |project_id, authorizations| authorizations.count > 1 }
                                      .keys
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AuthorizedProjectUpdate::FindRecordsDueForRefreshService, feature_category: :groups_and_projects do
  # We're using let! here so that any expectations for the service class are not
  # triggered twice.
  let!(:project) { create(:project) }

  let(:user) { project.namespace.owner }
  let(:service) { described_class.new(user) }

  describe '#execute' do
    context 'callbacks' do
      let(:callback) { double('callback') }

      context 'incorrect_auth_found_callback callback' do
        let(:user) { create(:user) }
        let(:service) do
          described_class.new(user, incorrect_auth_found_callback: callback)
        end

        it 'is called' do
          access_level = Gitlab::Access::DEVELOPER
          create(:project_authorization, user: user, project: project, access_level: access_level)

          expect(callback).to receive(:call).with(project.id, access_level).once

          service.execute
        end
      end

      context 'missing_auth_found_callback callback' do
        let(:service) do
          described_class.new(user, missing_auth_found_callback: callback)
        end

        it 'is called' do
          ProjectAuthorization.delete_all

          expect(callback).to receive(:call).with(project.id, Gitlab::Access::OWNER).once

          service.execute
        end
      end
    end

    context 'finding project authorizations due for refresh' do
      context 'when there are changes to be made' do
        before do
          user.project_authorizations.delete_all
        end

        it 'finds projects authorizations that needs to be refreshed' do
          project2 = create(:project)
          user.project_authorizations
            .create!(project: project2, access_level: Gitlab::Access::MAINTAINER)

          to_be_removed = [project2.id]
          to_be_added = [
            { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
          ]

          expect(service.execute).to eq([to_be_removed, to_be_added])
        end

        it 'finds entries with wrong access levels' do
          user.project_authorizations
            .create!(project: project, access_level: Gitlab::Access::DEVELOPER)

          to_be_removed = [project.id]
          to_be_added = [
            { user_id: user.id, project_id: project.id, access_level: Gitlab::Access::OWNER }
          ]

          expect(service.execute).to eq([to_be_removed, to_be_added])
        end
      end

      context 'when there are no changes to be made' do
        it 'returns empty arrays' do
          expect(service.execute).to eq([[], []])
        end
      end
    end
  end

  describe '#needs_refresh?' do
    subject { service.needs_refresh? }

    context 'when there are records due for either removal or addition' do
      context 'when there are both removals and additions to be made' do
        before do
          user.project_authorizations.delete_all
          create(:project_authorization, user: user)
        end

        it { is_expected.to eq(true) }
      end

      context 'when there are no removals, but there are additions to be made' do
        before do
          user.project_authorizations.delete_all
        end

        it { is_expected.to eq(true) }
      end

      context 'when there are no additions, but there are removals to be made' do
        before do
          create(:project_authorization, user: user)
        end

        it { is_expected.to eq(true) }
      end
    end

    context 'when there are no additions or removals to be made' do
      it { is_expected.to eq(false) }
    end
  end

  describe '#fresh_access_levels_per_project' do
    let(:hash) { service.fresh_access_levels_per_project }

    it 'returns a Hash' do
      expect(hash).to be_an_instance_of(Hash)
    end

    it 'sets the keys to the project IDs' do
      expect(hash.keys).to match_array([project.id])
    end

    it 'sets the values to the access levels' do
      expect(hash.values).to match_array([Gitlab::Access::OWNER])
    end

    context 'personal projects' do
      it 'includes the project with the right access level' do
        expect(hash[project.id]).to eq(Gitlab::Access::OWNER)
      end
    end

    context 'projects the user is a member of' do
      let!(:other_project) { create(:project) }

      before do
        other_project.team.add_reporter(user)
      end

      it 'includes the project with the right access level' do
        expect(hash[other_project.id]).to eq(Gitlab::Access::REPORTER)
      end
    end

    context 'projects of groups the user is a member of' do
      let(:group) { create(:group) }
      let!(:other_project) { create(:project, group: group) }

      before do
        group.add_owner(user)
      end

      it 'includes the project with the right access level' do
        expect(hash[other_project.id]).to eq(Gitlab::Access::OWNER)
      end
    end

    context 'projects of subgroups of groups the user is a member of' do
      let(:group) { create(:group) }
      let(:nested_group) { create(:group, parent: group) }
      let!(:other_project) { create(:project, group: nested_group) }

      before do
        group.add_maintainer(user)
      end

      it 'includes the project with the right access level' do
        expect(hash[other_project.id]).to eq(Gitlab::Access::MAINTAINER)
      end
    end

    context 'projects shared with groups the user is a member of' do
      let(:group) { create(:group) }
      let(:other_project) { create(:project) }
      let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) }

      before do
        group.add_maintainer(user)
      end

      it 'includes the project with the right access level' do
        expect(hash[other_project.id]).to eq(Gitlab::Access::GUEST)
      end
    end

    context 'projects shared with subgroups of groups the user is a member of' do
      let(:group) { create(:group) }
      let(:nested_group) { create(:group, parent: group) }
      let(:other_project) { create(:project) }
      let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) }

      before do
        group.add_maintainer(user)
      end

      it 'includes the project with the right access level' do
        expect(hash[other_project.id]).to eq(Gitlab::Access::DEVELOPER)
      end
    end
  end

  describe '#current_authorizations_per_project' do
    let(:hash) { service.current_authorizations_per_project }

    it 'returns a Hash' do
      expect(hash).to be_an_instance_of(Hash)
    end

    it 'sets the keys to the project IDs' do
      expect(hash.keys).to eq([project.id])
    end

    it 'sets the values to the project authorization rows' do
      expect(hash.values.length).to eq(1)

      value = hash.values[0]

      expect(value.project_id).to eq(project.id)
      expect(value.access_level).to eq(Gitlab::Access::OWNER)
    end
  end

  describe '#current_authorizations' do
    context 'without authorizations' do
      it 'returns an empty list' do
        user.project_authorizations.delete_all

        expect(service.current_authorizations.empty?).to eq(true)
      end
    end

    context 'with an authorization' do
      let(:row) { service.current_authorizations.take }

      it 'returns the currently authorized projects' do
        expect(service.current_authorizations.length).to eq(1)
      end

      it 'includes the project ID for every row' do
        expect(row.project_id).to eq(project.id)
      end

      it 'includes the access level for every row' do
        expect(row.access_level).to eq(Gitlab::Access::OWNER)
      end
    end
  end

  describe '#fresh_authorizations' do
    it 'returns the new authorized projects' do
      expect(service.fresh_authorizations.length).to eq(1)
    end

    it 'returns the highest access level' do
      project.team.add_guest(user)

      rows = service.fresh_authorizations.to_a

      expect(rows.length).to eq(1)
      expect(rows.first.access_level).to eq(Gitlab::Access::OWNER)
    end

    context 'every returned row' do
      let(:row) { service.fresh_authorizations.take }

      it 'includes the project ID' do
        expect(row.project_id).to eq(project.id)
      end

      it 'includes the access level' do
        expect(row.access_level).to eq(Gitlab::Access::OWNER)
      end
    end
  end
end


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

module AuthorizedProjectUpdate
  class ProjectRecalculateService
    # Service for refreshing all the authorizations to a particular project.
    include Gitlab::Utils::StrongMemoize
    BATCH_SIZE = 1000

    def initialize(project)
      @project = project
    end

    def execute
      refresh_authorizations if needs_refresh?
      ServiceResponse.success
    end

    private

    attr_reader :project

    def needs_refresh?
      user_ids_to_remove.any? ||
        authorizations_to_create.any?
    end

    def current_authorizations
      strong_memoize(:current_authorizations) do
        apply_scopes(project.project_authorizations)
          .pluck(:user_id, :access_level) # rubocop: disable CodeReuse/ActiveRecord
      end
    end

    def fresh_authorizations
      strong_memoize(:fresh_authorizations) do
        result = []

        effective_access_levels
          .each_batch(of: BATCH_SIZE, column: :user_id) do |member_batch|
            result += member_batch.pluck(:user_id, 'MAX(access_level)') # rubocop: disable CodeReuse/ActiveRecord
          end

        result
      end
    end

    def user_ids_to_remove
      strong_memoize(:user_ids_to_remove) do
        (current_authorizations - fresh_authorizations)
          .map { |user_id, _| user_id }
      end
    end

    def authorizations_to_create
      strong_memoize(:authorizations_to_create) do
        (fresh_authorizations - current_authorizations).map do |user_id, access_level|
          {
            user_id: user_id,
            access_level: access_level,
            project_id: project.id
          }
        end
      end
    end

    def refresh_authorizations
      ProjectAuthorizations::Changes.new do |changes|
        changes.add(authorizations_to_create)
        changes.remove_users_in_project(project, user_ids_to_remove)
      end.apply!
    end

    def apply_scopes(project_authorizations)
      project_authorizations
    end

    def effective_access_levels
      Projects::Members::EffectiveAccessLevelFinder.new(project).execute
    end
  end
end

AuthorizedProjectUpdate::ProjectRecalculateService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AuthorizedProjectUpdate::ProjectRecalculateService, '#execute', feature_category: :groups_and_projects do
  let_it_be(:project) { create(:project) }

  subject(:execute) { described_class.new(project).execute }

  it 'returns success' do
    expect(execute.success?).to eq(true)
  end

  context 'when there are no changes to be made' do
    it 'does not change authorizations' do
      expect { execute }.not_to(change { ProjectAuthorization.count })
    end
  end

  context 'when there are changes to be made' do
    let(:user) { create(:user) }

    context 'when addition is required' do
      before do
        project.add_developer(user)
        project.project_authorizations.where(user: user).delete_all
      end

      it 'adds a new authorization record' do
        expect { execute }.to(
          change { project.project_authorizations.where(user: user).count }
          .from(0).to(1)
        )
      end

      it 'adds a new authorization record with the correct access level' do
        execute

        project_authorization = project.project_authorizations.where(
          user: user,
          access_level: Gitlab::Access::DEVELOPER
        )

        expect(project_authorization).to exist
      end
    end

    context 'when removal is required' do
      before do
        create(:project_authorization, user: user, project: project)
      end

      it 'removes the authorization record' do
        expect { execute }.to(
          change { project.project_authorizations.where(user: user).count }
          .from(1).to(0)
        )
      end
    end

    context 'when an update in access level is required' do
      before do
        project.add_developer(user)
        project.project_authorizations.where(user: user).delete_all
        create(:project_authorization, project: project, user: user, access_level: Gitlab::Access::GUEST)
      end

      it 'updates the authorization of the user to the correct access level' do
        expect { execute }.to(
          change { project.project_authorizations.find_by(user: user).access_level }
            .from(Gitlab::Access::GUEST).to(Gitlab::Access::DEVELOPER)
        )
      end
    end
  end
end


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

module SystemNotes
  class IssuablesService < ::SystemNotes::BaseService
    # We create cross-referenced system notes when a commit relates to an issue.
    # There are two options what time to use for the system note:
    # 1. The push date (default)
    # 2. The commit date
    #
    # The commit date is useful when an existing Git repository is imported to GitLab.
    # It helps to preserve an original order of all notes (comments, commits, status changes, e.t.c)
    # in the imported issues. Otherwise, all commits will be linked before or after all other imported notes.
    #
    # See also the discussion in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60700#note_612724683
    USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE = false

    def self.issuable_events
      {
        assigned: s_('IssuableEvents|assigned to'),
        unassigned: s_('IssuableEvents|unassigned'),
        review_requested: s_('IssuableEvents|requested review from'),
        review_request_removed: s_('IssuableEvents|removed review request for')
      }.freeze
    end

    #
    # noteable_ref - Referenced noteable object, or array of objects
    #
    # Example Note text:
    #
    #   "marked this issue as related to gitlab-foss#9001"
    #   "marked this issue as related to gitlab-foss#9001, gitlab-foss#9002, and gitlab-foss#9003"
    #
    # Returns the created Note object
    def relate_issuable(noteable_ref)
      body = "marked this #{noteable_name} as related to #{extract_issuable_reference(noteable_ref)}"

      track_issue_event(:track_issue_related_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
    end

    #
    # noteable_ref - Referenced noteable object
    #
    # Example Note text:
    #
    #   "removed the relation with gitlab-foss#9001"
    #
    # Returns the created Note object
    def unrelate_issuable(noteable_ref)
      body = "removed the relation with #{noteable_ref.to_reference(noteable.resource_parent)}"

      track_issue_event(:track_issue_unrelated_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
    end

    # Called when the assignee of a Noteable is changed or removed
    #
    # assignee - User being assigned, or nil
    #
    # Example Note text:
    #
    #   "removed assignee"
    #
    #   "assigned to @rspeicher"
    #
    # Returns the created Note object
    def change_assignee(assignee)
      body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"

      track_issue_event(:track_issue_assignee_changed_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
    end

    # Called when the assignees of an issuable is changed or removed
    #
    # assignees - Users being assigned, or nil
    #
    # Example Note text:
    #
    #   "removed all assignees"
    #
    #   "assigned to @user1 additionally to @user2"
    #
    #   "assigned to @user1, @user2 and @user3 and unassigned @user4 and @user5"
    #
    #   "assigned to @user1 and @user2"
    #
    # Returns the created Note object
    def change_issuable_assignees(old_assignees)
      unassigned_users = old_assignees - noteable.assignees
      added_users = noteable.assignees.to_a - old_assignees
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "#{self.class.issuable_events[:assigned]} #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
        text_parts << "#{self.class.issuable_events[:unassigned]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
      end

      body = text_parts.join(' and ')

      track_issue_event(:track_issue_assignee_changed_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
    end

    # Called when the reviewers of an issuable is changed or removed
    #
    # reviewers - Users being requested to review, or nil
    #
    # Example Note text:
    #
    #   "requested review from @user1 and @user2"
    #
    #   "requested review from @user1, @user2 and @user3 and removed review request for @user4 and @user5"
    #
    # Returns the created Note object
    def change_issuable_reviewers(old_reviewers)
      unassigned_users = old_reviewers - noteable.reviewers
      added_users = noteable.reviewers - old_reviewers
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "#{self.class.issuable_events[:review_requested]} #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
        text_parts << "#{self.class.issuable_events[:review_request_removed]} #{unassigned_users.map(&:to_reference).to_sentence}" if unassigned_users.any?
      end

      body = text_parts.join(' and ')

      create_note(NoteSummary.new(noteable, project, author, body, action: 'reviewer'))
    end

    # Called when the contacts of an issuable are changed or removed
    # We intend to reference the contacts but for security we are just
    # going to state how many were added/removed for now. See discussion:
    # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77816#note_806114273
    #
    # added_count - number of contacts added, or 0
    # removed_count - number of contacts  removed, or 0
    #
    # Example Note text:
    #
    #   "added 2 contacts"
    #
    #   "added 3 contacts and removed one contact"
    #
    # Returns the created Note object
    def change_issuable_contacts(added_count, removed_count)
      text_parts = []

      Gitlab::I18n.with_default_locale do
        text_parts << "added #{added_count} #{'contact'.pluralize(added_count)}" if added_count > 0
        text_parts << "removed #{removed_count} #{'contact'.pluralize(removed_count)}" if removed_count > 0
      end

      return if text_parts.empty?

      body = text_parts.join(' and ')
      create_note(NoteSummary.new(noteable, project, author, body, action: 'contact'))
    end

    # Called when the title of a Noteable is changed
    #
    # old_title - Previous String title
    #
    # Example Note text:
    #
    #   "changed title from **Old** to **New**"
    #
    # Returns the created Note object
    def change_title(old_title)
      new_title = noteable.title.dup

      old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs

      marked_old_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(old_title).mark(old_diffs)
      marked_new_title = Gitlab::Diff::InlineDiffMarkdownMarker.new(new_title).mark(new_diffs)

      body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"

      track_issue_event(:track_issue_title_changed_action)
      work_item_activity_counter.track_work_item_title_changed_action(author: author) if noteable.is_a?(WorkItem)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
    end

    # Called when the hierarchy of a work item is changed
    #
    # noteable  - Noteable object that responds to `work_item_parent` and `work_item_children`
    # project   - Project owning noteable
    # author    - User performing the change
    #
    # Example Note text:
    #
    #   "added #1 as child Task"
    #
    # Returns the created Note object
    def hierarchy_changed(work_item, action)
      params = hierarchy_note_params(action, noteable, work_item)

      create_note(NoteSummary.new(noteable, project, author, params[:parent_note_body], action: params[:parent_action]))
      create_note(NoteSummary.new(work_item, project, author, params[:child_note_body], action: params[:child_action]))
    end

    # Called when the description of a Noteable is changed
    #
    # noteable  - Noteable object that responds to `description`
    # project   - Project owning noteable
    # author    - User performing the change
    #
    # Example Note text:
    #
    #   "changed the description"
    #
    # Returns the created Note object
    def change_description
      body = 'changed the description'

      track_issue_event(:track_issue_description_changed_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'description'))
    end

    # Called when a Mentionable (the `mentioned_in`) references another Mentionable (the `mentioned`,
    # passed to this service as `noteable`).
    #
    # Example Note text:
    #
    #   "mentioned in #1"
    #
    #   "mentioned in !2"
    #
    #   "mentioned in 54f7727c"
    #
    # See cross_reference_note_content.
    #
    # @param mentioned_in [Mentionable]
    # @return [Note]
    def cross_reference(mentioned_in)
      return if cross_reference_disallowed?(mentioned_in)

      gfm_reference = mentioned_in.gfm_reference(noteable.project || noteable.group)
      body = cross_reference_note_content(gfm_reference)

      if noteable.is_a?(ExternalIssue)
        Integrations::CreateExternalCrossReferenceWorker.perform_async(
          noteable.project_id,
          noteable.id,
          mentioned_in.class.name,
          mentioned_in.id,
          author.id
        )
      else
        track_cross_reference_action

        created_at = mentioner.created_at if USE_COMMIT_DATE_FOR_CROSS_REFERENCE_NOTE && mentioner.is_a?(Commit)
        create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference', created_at: created_at))
      end
    end

    # Check if a cross-reference is disallowed
    #
    # This method prevents adding a "mentioned in !1" note on every single commit
    # in a merge request. Additionally, it prevents the creation of references to
    # external issues (which would fail).
    #
    # @param mentioned_in [Mentionable]
    # @return [Boolean]
    def cross_reference_disallowed?(mentioned_in)
      return true if noteable.is_a?(ExternalIssue) && !noteable.project&.external_references_supported?
      return false unless mentioned_in.is_a?(MergeRequest)
      return false unless noteable.is_a?(Commit)

      mentioned_in.commits.include?(noteable)
    end

    # Called when the status of a Task has changed
    #
    # new_task  - TaskList::Item object.
    #
    # Example Note text:
    #
    #   "marked the checklist item Whatever as completed."
    #
    # Returns the created Note object
    def change_task_status(new_task)
      status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
      body = "marked the checklist item **#{new_task.source}** as #{status_label}"

      track_issue_event(:track_issue_description_changed_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
    end

    # Called when noteable has been moved to another project
    #
    # noteable_ref - Referenced noteable
    # direction    - symbol, :to or :from
    #
    # Example Note text:
    #
    #   "moved to some_namespace/project_new#11"
    #
    # Returns the created Note object
    def noteable_moved(noteable_ref, direction)
      unless [:to, :from].include?(direction)
        raise ArgumentError, "Invalid direction `#{direction}`"
      end

      cross_reference = noteable_ref.to_reference(project)
      body = "moved #{direction} #{cross_reference}"

      track_issue_event(:track_issue_moved_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
    end

    # Called when noteable has been cloned
    #
    # noteable_ref - Referenced noteable
    # direction    - symbol, :to or :from
    # created_at   - timestamp for the system note, defaults to current time
    #
    # Example Note text:
    #
    #   "cloned to some_namespace/project_new#11"
    #
    # Returns the created Note object
    def noteable_cloned(noteable_ref, direction, created_at: nil)
      unless [:to, :from].include?(direction)
        raise ArgumentError, "Invalid direction `#{direction}`"
      end

      cross_reference = noteable_ref.to_reference(project)
      body = "cloned #{direction} #{cross_reference}"

      track_issue_event(:track_issue_cloned_action) if direction == :to

      create_note(NoteSummary.new(noteable, project, author, body, action: 'cloned', created_at: created_at))
    end

    # Called when the confidentiality changes
    #
    # Example Note text:
    #
    #   "made the issue confidential"
    #
    # Returns the created Note object
    def change_issue_confidentiality
      if noteable.confidential
        body = "made the #{noteable_name} confidential"
        action = 'confidential'

        track_issue_event(:track_issue_made_confidential_action)
      else
        body = "made the #{noteable_name} visible to everyone"
        action = 'visible'

        track_issue_event(:track_issue_made_visible_action)
      end

      create_note(NoteSummary.new(noteable, project, author, body, action: action))
    end

    # Called when the status of a Noteable is changed
    #
    # status   - String status
    # source   - Mentionable performing the change, or nil
    #
    # Example Note text:
    #
    #   "merged"
    #
    #   "closed via bc17db76"
    #
    # Returns the created Note object
    def change_status(status, source = nil)
      create_resource_state_event(status: status, mentionable_source: source)
    end

    # Check if a cross reference to a Mentionable from the `mentioned_in` Mentionable
    # already exists.
    #
    # This method is used to prevent multiple notes being created for a mention
    # when a issue is updated, for example. The method also calls `existing_mentions_for`
    # to check if the mention is in a commit, and return matches only on commit hash
    # instead of project + commit, to avoid repeated mentions from forks.
    #
    # @param mentioned_in [Mentionable]
    # @return [Boolean]
    def cross_reference_exists?(mentioned_in)
      notes = noteable.notes.system
      existing_mentions_for(mentioned_in, noteable, notes).exists?
    end

    # Called when a Noteable has been marked as the canonical Issue of a duplicate
    #
    # duplicate_issue - Issue that was a duplicate of this
    #
    # Example Note text:
    #
    #   "marked #1234 as a duplicate of this issue"
    #
    #   "marked other_project#5678 as a duplicate of this issue"
    #
    # Returns the created Note object
    def mark_canonical_issue_of_duplicate(duplicate_issue)
      body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
      create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
    end

    # Called when a Noteable has been marked as a duplicate of another Issue
    #
    # canonical_issue - Issue that this is a duplicate of
    #
    # Example Note text:
    #
    #   "marked this issue as a duplicate of #1234"
    #
    #   "marked this issue as a duplicate of other_project#5678"
    #
    # Returns the created Note object
    def mark_duplicate_issue(canonical_issue)
      body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"

      track_issue_event(:track_issue_marked_as_duplicate_action)

      create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
    end

    def add_email_participants(body)
      create_note(NoteSummary.new(noteable, project, author, body))
    end

    def discussion_lock
      action = noteable.discussion_locked? ? 'locked' : 'unlocked'
      body = "#{action} the discussion in this #{noteable.class.to_s.titleize.downcase}"

      if action == 'locked'
        track_issue_event(:track_issue_locked_action)
      else
        track_issue_event(:track_issue_unlocked_action)
      end

      create_note(NoteSummary.new(noteable, project, author, body, action: action))
    end

    def close_after_error_tracking_resolve
      create_resource_state_event(status: 'closed', close_after_error_tracking_resolve: true)
    end

    def auto_resolve_prometheus_alert
      create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true)
    end

    def change_issue_type(previous_type)
      previous = previous_type.humanize(capitalize: false)
      new = noteable.issue_type.humanize(capitalize: false)
      body = "changed type from #{previous} to #{new}"

      create_note(NoteSummary.new(noteable, project, author, body, action: 'issue_type'))
    end

    private

    def cross_reference_note_content(gfm_reference)
      "#{self.class.cross_reference_note_prefix}#{gfm_reference}"
    end

    def existing_mentions_for(mentioned_in, noteable, notes)
      if mentioned_in.is_a?(Commit)
        text = "#{self.class.cross_reference_note_prefix}%#{mentioned_in.to_reference(nil)}"
        notes.like_note_or_capitalized_note(text)
      else
        gfm_reference = mentioned_in.gfm_reference(noteable.project || noteable.group)
        text = cross_reference_note_content(gfm_reference)
        notes.for_note_or_capitalized_note(text)
      end
    end

    def self.cross_reference_note_prefix
      'mentioned in '
    end

    def self.cross_reference?(note_text)
      note_text =~ /\A#{cross_reference_note_prefix}/i
    end

    def create_resource_state_event(params)
      ResourceEvents::ChangeStateService.new(resource: noteable, user: author)
        .execute(params)
    end

    def issue_activity_counter
      Gitlab::UsageDataCounters::IssueActivityUniqueCounter
    end

    def work_item_activity_counter
      Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter
    end

    def track_cross_reference_action
      track_issue_event(:track_issue_cross_referenced_action)
    end

    def hierarchy_note_params(action, parent, child)
      return {} unless child && parent

      child_type = child.issue_type.humanize(capitalize: false)
      parent_type = parent.issue_type.humanize(capitalize: false)

      if action == 'relate'
        {
          parent_note_body: "added #{child.to_reference} as child #{child_type}",
          child_note_body: "added #{parent.to_reference} as parent #{parent_type}",
          parent_action: 'relate_to_child',
          child_action: 'relate_to_parent'
        }
      else
        {
          parent_note_body: "removed child #{child_type} #{child.to_reference}",
          child_note_body: "removed parent #{parent_type} #{parent.to_reference}",
          parent_action: 'unrelate_from_child',
          child_action: 'unrelate_from_parent'
        }
      end
    end

    def track_issue_event(event_name)
      return unless noteable.is_a?(Issue)

      issue_activity_counter.public_send(event_name, author: author, project: project || noteable.project) # rubocop: disable GitlabSecurity/PublicSend
    end

    def noteable_name
      name = noteable.try(:issue_type) || noteable.to_ability_name

      name.humanize(capitalize: false)
    end

    def extract_issuable_reference(reference)
      if reference.is_a?(Array)
        reference.map { |item| item.to_reference(noteable.resource_parent) }.to_sentence
      else
        reference.to_reference(noteable.resource_parent)
      end
    end
  end
end

SystemNotes::IssuablesService.prepend_mod_with('SystemNotes::IssuablesService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning do
  include ProjectForksHelper

  let_it_be(:group)   { create(:group) }
  let_it_be(:project) { create(:project, :repository, group: group) }
  let_it_be(:author)  { create(:user) }

  let(:noteable)      { create(:issue, project: project) }
  let(:issue)         { noteable }

  let(:service) { described_class.new(noteable: noteable, project: project, author: author) }

  describe '#relate_issuable' do
    let_it_be(:issue1) { create(:issue, project: project) }
    let_it_be(:issue2) { create(:issue, project: project) }

    let(:noteable_ref) { issue1 }

    subject(:system_note) { service.relate_issuable(noteable_ref) }

    context 'when issue marks another as related' do
      it_behaves_like 'a system note' do
        let(:action) { 'relate' }
      end

      it 'sets the note text' do
        expect(system_note.note).to eq "marked this issue as related to #{issue1.to_reference(project)}"
      end
    end

    context 'when issue marks several other issues as related' do
      let(:noteable_ref) { [issue1, issue2] }

      it_behaves_like 'a system note' do
        let(:action) { 'relate' }
      end

      it 'sets the note text' do
        expect(system_note.note).to eq(
          "marked this issue as related to #{issue1.to_reference(project)} and #{issue2.to_reference(project)}"
        )
      end
    end

    context 'with work items' do
      let_it_be(:noteable) { create(:work_item, :task, project: project) }

      it 'sets the note text with the correct work item type' do
        expect(subject.note).to eq "marked this task as related to #{noteable_ref.to_reference(project)}"
      end
    end
  end

  describe '#unrelate_issuable' do
    let(:noteable_ref) { create(:issue) }

    subject { service.unrelate_issuable(noteable_ref) }

    it_behaves_like 'a system note' do
      let(:action) { 'unrelate' }
    end

    context 'when issue relation is removed' do
      it 'sets the note text' do
        expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
      end
    end
  end

  describe '#change_assignee' do
    subject { service.change_assignee(assignee) }

    let(:assignee) { create(:user) }

    it_behaves_like 'a system note' do
      let(:action) { 'assignee' }
    end

    context 'when assignee added' do
      it_behaves_like 'a note with overridable created_at'

      it 'sets the note text' do
        expect(subject.note).to eq "assigned to @#{assignee.username}"
      end
    end

    context 'when assignee removed' do
      let(:assignee) { nil }

      it_behaves_like 'a note with overridable created_at'

      it 'sets the note text' do
        expect(subject.note).to eq 'removed assignee'
      end
    end
  end

  describe '#change_issuable_assignees' do
    subject { service.change_issuable_assignees([assignee]) }

    let(:assignee) { create(:user) }
    let(:assignee1) { create(:user) }
    let(:assignee2) { create(:user) }
    let(:assignee3) { create(:user) }

    it_behaves_like 'a system note' do
      let(:action) { 'assignee' }
    end

    def build_note(old_assignees, new_assignees)
      issue.assignees = new_assignees
      service.change_issuable_assignees(old_assignees).note
    end

    it_behaves_like 'a note with overridable created_at'

    it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
      expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
    end

    it 'builds a correct phrase when assignee removed' do
      expect(build_note([assignee1], [])).to eq "unassigned @#{assignee1.username}"
    end

    it 'builds a correct phrase when assignees changed' do
      expect(build_note([assignee1], [assignee2])).to eq \
        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
    end

    it 'builds a correct phrase when three assignees removed and one added' do
      expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
        "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
    end

    it 'builds a correct phrase when one assignee changed from a set' do
      expect(build_note([assignee, assignee1], [assignee, assignee2])).to eq \
        "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}"
    end

    it 'builds a correct phrase when one assignee removed from a set' do
      expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \
        "unassigned @#{assignee2.username}"
    end

    it 'builds a correct phrase when the locale is different' do
      Gitlab::I18n.with_locale('pt-BR') do
        expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \
          "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}"
      end
    end
  end

  describe '#change_issuable_reviewers' do
    subject { service.change_issuable_reviewers([reviewer]) }

    let_it_be(:noteable) { create(:merge_request, :simple, source_project: project) }
    let_it_be(:reviewer) { create(:user) }
    let_it_be(:reviewer1) { create(:user) }
    let_it_be(:reviewer2) { create(:user) }
    let_it_be(:reviewer3) { create(:user) }

    it_behaves_like 'a system note' do
      let(:action) { 'reviewer' }
    end

    def build_note(old_reviewers, new_reviewers)
      noteable.reviewers = new_reviewers
      service.change_issuable_reviewers(old_reviewers).note
    end

    it 'builds a correct phrase when a reviewer is added to a non-assigned merge request' do
      expect(build_note([], [reviewer1])).to eq "requested review from @#{reviewer1.username}"
    end

    it 'builds a correct phrase when reviewer is removed' do
      expect(build_note([reviewer], [])).to eq "removed review request for @#{reviewer.username}"
    end

    it 'builds a correct phrase when reviewers changed' do
      expect(build_note([reviewer1], [reviewer2])).to(
        eq("requested review from @#{reviewer2.username} and removed review request for @#{reviewer1.username}")
      )
    end

    it 'builds a correct phrase when three reviewers removed and one added' do
      expect(build_note([reviewer, reviewer1, reviewer2], [reviewer3])).to(
        eq("requested review from @#{reviewer3.username} and removed review request for @#{reviewer.username}, @#{reviewer1.username}, and @#{reviewer2.username}")
      )
    end

    it 'builds a correct phrase when one reviewer is changed from a set' do
      expect(build_note([reviewer, reviewer1], [reviewer, reviewer2])).to(
        eq("requested review from @#{reviewer2.username} and removed review request for @#{reviewer1.username}")
      )
    end

    it 'builds a correct phrase when one reviewer removed from a set' do
      expect(build_note([reviewer, reviewer1, reviewer2], [reviewer, reviewer1])).to(
        eq("removed review request for @#{reviewer2.username}")
      )
    end

    it 'builds a correct phrase when the locale is different' do
      Gitlab::I18n.with_locale('pt-BR') do
        expect(build_note([reviewer, reviewer1, reviewer2], [reviewer3])).to(
          eq("requested review from @#{reviewer3.username} and removed review request for @#{reviewer.username}, @#{reviewer1.username}, and @#{reviewer2.username}")
        )
      end
    end
  end

  describe '#change_issuable_contacts' do
    subject { service.change_issuable_contacts(1, 1) }

    let_it_be(:noteable) { create(:issue, project: project) }

    it_behaves_like 'a system note' do
      let(:action) { 'contact' }
    end

    def build_note(added_count, removed_count)
      service.change_issuable_contacts(added_count, removed_count).note
    end

    it 'builds a correct phrase when one contact is added' do
      expect(build_note(1, 0)).to eq "added 1 contact"
    end

    it 'builds a correct phrase when one contact is removed' do
      expect(build_note(0, 1)).to eq "removed 1 contact"
    end

    it 'builds a correct phrase when one contact is added and one contact is removed' do
      expect(build_note(1, 1)).to(
        eq("added 1 contact and removed 1 contact")
      )
    end

    it 'builds a correct phrase when three contacts are added and one removed' do
      expect(build_note(3, 1)).to(
        eq("added 3 contacts and removed 1 contact")
      )
    end

    it 'builds a correct phrase when three contacts are removed and one added' do
      expect(build_note(1, 3)).to(
        eq("added 1 contact and removed 3 contacts")
      )
    end

    it 'builds a correct phrase when the locale is different' do
      Gitlab::I18n.with_locale('pt-BR') do
        expect(build_note(1, 3)).to(
          eq("added 1 contact and removed 3 contacts")
        )
      end
    end
  end

  describe '#change_status' do
    subject { service.change_status(status, source) }

    let(:status) { 'reopened' }
    let(:source) { nil }

    it 'creates a resource state event' do
      expect { subject }.to change { ResourceStateEvent.count }.by(1)
    end
  end

  describe '#change_title' do
    let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }

    subject { service.change_title('Old title') }

    context 'when noteable responds to `title`' do
      it_behaves_like 'a system note' do
        let(:action) { 'title' }
      end

      it_behaves_like 'a note with overridable created_at'

      it 'sets the note text' do
        expect(subject.note)
          .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
      end
    end
  end

  describe '#change_description' do
    subject { service.change_description }

    context 'when noteable responds to `description`' do
      it_behaves_like 'a system note' do
        let(:action) { 'description' }
      end

      it_behaves_like 'a note with overridable created_at'

      it 'sets the note text' do
        expect(subject.note).to eq('changed the description')
      end

      it 'associates the related description version' do
        noteable.update!(description: 'New description')

        description_version_id = subject.system_note_metadata.description_version_id

        expect(description_version_id).not_to be_nil
        expect(description_version_id).to eq(noteable.saved_description_version.id)
      end
    end
  end

  describe '#change_issue_confidentiality' do
    subject { service.change_issue_confidentiality }

    context 'issue has been made confidential' do
      before do
        noteable.update_attribute(:confidential, true)
      end

      it_behaves_like 'a system note' do
        let(:action) { 'confidential' }
      end

      it 'sets the note text' do
        expect(subject.note).to eq 'made the issue confidential'
      end
    end

    context 'issue has been made visible' do
      it_behaves_like 'a system note' do
        let(:action) { 'visible' }
      end

      it 'sets the note text' do
        expect(subject.note).to eq 'made the issue visible to everyone'
      end
    end
  end

  describe '#cross_reference' do
    let(:service) { described_class.new(noteable: noteable, author: author) }

    let(:mentioned_in) { create(:issue, project: project) }

    subject { service.cross_reference(mentioned_in) }

    it_behaves_like 'a system note' do
      let(:action) { 'cross_reference' }
    end

    context 'when cross-reference disallowed' do
      before do
        expect_next_instance_of(described_class) do |instance|
          expect(instance).to receive(:cross_reference_disallowed?).and_return(true)
        end
      end

      it 'returns nil' do
        expect(subject).to be_nil
      end

      it 'does not create a system note metadata record' do
        expect { subject }.not_to change { SystemNoteMetadata.count }
      end
    end

    context 'when cross-reference allowed' do
      before do
        expect_next_instance_of(described_class) do |instance|
          expect(instance).to receive(:cross_reference_disallowed?).and_return(false)
        end
      end

      it_behaves_like 'a system note' do
        let(:action) { 'cross_reference' }
      end

      it_behaves_like 'a note with overridable created_at'

      describe 'note_body' do
        context 'cross-project' do
          let(:project2) { create(:project, :repository) }
          let(:mentioned_in) { create(:issue, project: project2) }

          context 'from Commit' do
            let(:mentioned_in) { project2.repository.commit }

            it 'references the mentioning commit' do
              expect(subject.note).to eq "mentioned in commit #{mentioned_in.to_reference(project)}"
            end
          end

          context 'from non-Commit' do
            it 'references the mentioning object' do
              expect(subject.note).to eq "mentioned in issue #{mentioned_in.to_reference(project)}"
            end
          end
        end

        context 'within the same project' do
          context 'from Commit' do
            let(:mentioned_in) { project.repository.commit }

            it 'references the mentioning commit' do
              expect(subject.note).to eq "mentioned in commit #{mentioned_in.to_reference}"
            end
          end

          context 'from non-Commit' do
            it 'references the mentioning object' do
              expect(subject.note).to eq "mentioned in issue #{mentioned_in.to_reference}"
            end
          end
        end
      end

      context 'with external issue' do
        let(:noteable) { ExternalIssue.new('JIRA-123', project) }
        let(:mentioned_in) { project.commit }

        it 'queues a background worker' do
          expect(Integrations::CreateExternalCrossReferenceWorker).to receive(:perform_async).with(
            project.id,
            'JIRA-123',
            'Commit',
            mentioned_in.id,
            author.id
          )

          subject
        end
      end
    end
  end

  describe '#cross_reference_exists?' do
    let(:commit0) { project.commit }
    let(:commit1) { project.commit('HEAD~2') }

    context 'issue from commit' do
      before do
        # Mention issue (noteable) from commit0
        service.cross_reference(commit0)
      end

      it 'is truthy when already mentioned' do
        expect(service.cross_reference_exists?(commit0))
          .to be_truthy
      end

      it 'is falsey when not already mentioned' do
        expect(service.cross_reference_exists?(commit1))
          .to be_falsey
      end

      context 'legacy capitalized cross reference' do
        before do
          # Mention issue (noteable) from commit0
          system_note = service.cross_reference(commit0)
          system_note.update!(note: system_note.note.capitalize)
        end

        it 'is truthy when already mentioned' do
          expect(service.cross_reference_exists?(commit0))
            .to be_truthy
        end
      end
    end

    context 'commit from commit' do
      let(:service) { described_class.new(noteable: commit0, author: author) }

      before do
        # Mention commit1 from commit0
        service.cross_reference(commit1)
      end

      it 'is truthy when already mentioned' do
        expect(service.cross_reference_exists?(commit1))
          .to be_truthy
      end

      it 'is falsey when not already mentioned' do
        service = described_class.new(noteable: commit1, author: author)

        expect(service.cross_reference_exists?(commit0))
          .to be_falsey
      end

      context 'legacy capitalized cross reference' do
        before do
          # Mention commit1 from commit0
          system_note = service.cross_reference(commit1)
          system_note.update!(note: system_note.note.capitalize)
        end

        it 'is truthy when already mentioned' do
          expect(service.cross_reference_exists?(commit1))
            .to be_truthy
        end
      end
    end

    context 'commit with cross-reference from fork', :sidekiq_might_not_need_inline do
      let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
      let(:forked_project) { fork_project(project, author2, repository: true) }
      let(:commit2) { forked_project.commit }

      let(:service) { described_class.new(noteable: noteable, author: author2) }

      before do
        service.cross_reference(commit0)
      end

      it 'is true when a fork mentions an external issue' do
        expect(service.cross_reference_exists?(commit2))
            .to be true
      end

      context 'legacy capitalized cross reference' do
        before do
          system_note = service.cross_reference(commit0)
          system_note.update!(note: system_note.note.capitalize)
        end

        it 'is true when a fork mentions an external issue' do
          expect(service.cross_reference_exists?(commit2))
              .to be true
        end
      end
    end
  end

  describe '#change_task_status' do
    let(:noteable) { create(:issue, project: project) }
    let(:task)     { double(:task, complete?: true, source: 'task') }

    subject { service.change_task_status(task) }

    it_behaves_like 'a system note' do
      let(:action) { 'task' }
    end

    it "posts the 'marked the checklist item as complete' system note" do
      expect(subject.note).to eq("marked the checklist item **task** as completed")
    end
  end

  describe '#noteable_moved' do
    let(:new_project) { create(:project) }
    let(:new_noteable) { create(:issue, project: new_project) }

    subject do
      # service = described_class.new(noteable: noteable, project: project, author: author)
      service.noteable_moved(new_noteable, direction)
    end

    shared_examples 'cross project mentionable' do
      include MarkupHelper

      it 'contains cross reference to new noteable' do
        expect(subject.note).to include cross_project_reference(new_project, new_noteable)
      end

      it 'mentions referenced noteable' do
        expect(subject.note).to include new_noteable.to_reference
      end

      it 'mentions referenced project' do
        expect(subject.note).to include new_project.full_path
      end
    end

    context 'moved to' do
      let(:direction) { :to }

      it_behaves_like 'cross project mentionable'
      it_behaves_like 'a system note' do
        let(:action) { 'moved' }
      end

      it 'notifies about noteable being moved to' do
        expect(subject.note).to match('moved to')
      end
    end

    context 'moved from' do
      let(:direction) { :from }

      it_behaves_like 'cross project mentionable'
      it_behaves_like 'a system note' do
        let(:action) { 'moved' }
      end

      it 'notifies about noteable being moved from' do
        expect(subject.note).to match('moved from')
      end
    end

    context 'invalid direction' do
      let(:direction) { :invalid }

      it 'raises error' do
        expect { subject }.to raise_error StandardError, /Invalid direction/
      end
    end
  end

  describe '#noteable_cloned' do
    let_it_be(:new_project) { create(:project) }
    let_it_be(:new_noteable) { create(:issue, project: new_project) }

    subject do
      service.noteable_cloned(new_noteable, direction)
    end

    shared_examples 'cross project mentionable' do
      include MarkupHelper

      it 'contains cross reference to new noteable' do
        expect(subject.note).to include cross_project_reference(new_project, new_noteable)
      end

      it 'mentions referenced noteable' do
        expect(subject.note).to include new_noteable.to_reference
      end

      it 'mentions referenced project' do
        expect(subject.note).to include new_project.full_path
      end
    end

    context 'cloned to' do
      let(:direction) { :to }

      it_behaves_like 'cross project mentionable'

      it_behaves_like 'a system note' do
        let(:action) { 'cloned' }
      end

      it 'notifies about noteable being cloned to' do
        expect(subject.note).to match('cloned to')
      end
    end

    context 'cloned from' do
      let(:direction) { :from }

      it_behaves_like 'cross project mentionable'

      it_behaves_like 'a system note' do
        let(:action) { 'cloned' }
      end

      it 'notifies about noteable being cloned from' do
        expect(subject.note).to match('cloned from')
      end
    end

    context 'invalid direction' do
      let(:direction) { :invalid }

      it 'raises error' do
        expect { subject }.to raise_error StandardError, /Invalid direction/
      end
    end

    context 'custom created timestamp' do
      let(:direction) { :from }

      it 'allows setting of custom created_at value' do
        timestamp = 1.day.ago

        note = service.noteable_cloned(new_noteable, direction, created_at: timestamp)

        expect(note.created_at).to be_like_time(timestamp)
      end

      it 'defaults to current time when created_at is not given', :freeze_time do
        expect(subject.created_at).to be_like_time(Time.current)
      end
    end

    context 'metrics' do
      context 'cloned from' do
        let(:direction) { :from }

        it 'does not tracks usage' do
          expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
            .not_to receive(:track_issue_cloned_action).with(author: author)

          subject
        end
      end

      context 'cloned to', :snowplow do
        let(:direction) { :to }

        it 'tracks usage' do
          expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter)
            .to receive(:track_issue_cloned_action).with(author: author, project: project)

          subject
        end

        it_behaves_like 'internal event tracking' do
          let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CLONED }
          let(:user) { author }
          let(:namespace) { project.namespace }
        end
      end
    end
  end

  describe '#mark_duplicate_issue' do
    subject { service.mark_duplicate_issue(canonical_issue) }

    context 'within the same project' do
      let(:canonical_issue) { create(:issue, project: project) }

      it_behaves_like 'a system note' do
        let(:action) { 'duplicate' }
      end

      it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" }
    end

    context 'across different projects' do
      let(:other_project) { create(:project) }
      let(:canonical_issue) { create(:issue, project: other_project) }

      it_behaves_like 'a system note' do
        let(:action) { 'duplicate' }
      end

      it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" }
    end
  end

  describe '#mark_canonical_issue_of_duplicate' do
    subject { service.mark_canonical_issue_of_duplicate(duplicate_issue) }

    context 'within the same project' do
      let(:duplicate_issue) { create(:issue, project: project) }

      it_behaves_like 'a system note' do
        let(:action) { 'duplicate' }
      end

      it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" }
    end

    context 'across different projects' do
      let(:other_project) { create(:project) }
      let(:duplicate_issue) { create(:issue, project: other_project) }

      it_behaves_like 'a system note' do
        let(:action) { 'duplicate' }
      end

      it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
    end
  end

  describe '#discussion_lock' do
    subject { service.discussion_lock }

    context 'discussion unlocked' do
      it_behaves_like 'a system note' do
        let(:action) { 'unlocked' }
      end

      it 'creates the note text correctly' do
        [:issue, :merge_request].each do |type|
          issuable = create(type) # rubocop:disable Rails/SaveBang

          service = described_class.new(noteable: issuable, author: author)
          expect(service.discussion_lock.note)
            .to eq("unlocked the discussion in this #{type.to_s.titleize.downcase}")
        end
      end
    end

    context 'discussion locked' do
      before do
        noteable.update_attribute(:discussion_locked, true)
      end

      it_behaves_like 'a system note' do
        let(:action) { 'locked' }
      end

      it 'creates the note text correctly' do
        [:issue, :merge_request].each do |type|
          issuable = create(type, discussion_locked: true)

          service = described_class.new(noteable: issuable, author: author)
          expect(service.discussion_lock.note)
            .to eq("locked the discussion in this #{type.to_s.titleize.downcase}")
        end
      end
    end
  end

  describe '#cross_reference_disallowed?' do
    context 'when mentioned_in is not a MergeRequest' do
      it 'is falsey' do
        mentioned_in = noteable.dup

        expect(service.cross_reference_disallowed?(mentioned_in)).to be_falsey
      end
    end

    context 'when mentioned_in is a MergeRequest' do
      let(:mentioned_in) { create(:merge_request, :simple, source_project: project) }
      let(:noteable) { project.commit }

      it 'is truthy when noteable is in commits' do
        expect(mentioned_in).to receive(:commits).and_return([noteable])

        expect(service.cross_reference_disallowed?(mentioned_in)).to be_truthy
      end

      it 'is falsey when noteable is not in commits' do
        expect(mentioned_in).to receive(:commits).and_return([])

        expect(service.cross_reference_disallowed?(mentioned_in)).to be_falsey
      end
    end

    context 'when notable is an ExternalIssue' do
      let(:project) { create(:project) }
      let(:noteable) { ExternalIssue.new('EXT-1234', project) }

      it 'is false with issue tracker supporting referencing' do
        create(:jira_integration, project: project)
        project.reload

        expect(service.cross_reference_disallowed?(noteable)).to be_falsey
      end

      it 'is true with issue tracker not supporting referencing' do
        create(:bugzilla_integration, project: project)
        project.reload

        expect(service.cross_reference_disallowed?(noteable)).to be_truthy
      end

      it 'is true without issue tracker' do
        expect(service.cross_reference_disallowed?(noteable)).to be_truthy
      end
    end
  end

  describe '#close_after_error_tracking_resolve' do
    subject { service.close_after_error_tracking_resolve }

    it 'creates the expected state event' do
      subject

      event = ResourceStateEvent.last

      expect(event.close_after_error_tracking_resolve).to eq(true)
      expect(event.state).to eq('closed')
    end
  end

  describe '#auto_resolve_prometheus_alert' do
    subject { service.auto_resolve_prometheus_alert }

    it 'creates the expected state event' do
      subject

      event = ResourceStateEvent.last

      expect(event.close_auto_resolve_prometheus_alert).to eq(true)
      expect(event.state).to eq('closed')
    end
  end

  describe '#change_issue_type' do
    context 'with issue' do
      let_it_be_with_reload(:noteable) { create(:issue, project: project) }

      subject { service.change_issue_type('incident') }

      it_behaves_like 'a system note' do
        let(:action) { 'issue_type' }
      end

      it { expect(subject.note).to eq "changed type from incident to issue" }
    end

    context 'with work item' do
      let_it_be_with_reload(:noteable) { create(:work_item, project: project) }

      subject { service.change_issue_type('task') }

      it_behaves_like 'a system note' do
        let(:action) { 'issue_type' }
      end

      it { expect(subject.note).to eq "changed type from task to issue" }
    end
  end

  describe '#hierarchy_changed' do
    let_it_be_with_reload(:work_item) { create(:work_item, project: project) }
    let_it_be_with_reload(:task) { create(:work_item, :task, project: project) }

    let(:service) { described_class.new(noteable: work_item, project: project, author: author) }

    subject { service.hierarchy_changed(task, hierarchy_change_action) }

    context 'when task is added as a child' do
      let(:hierarchy_change_action) { 'relate' }

      it_behaves_like 'a system note' do
        let(:expected_noteable) { task }
        let(:action) { 'relate_to_parent' }
      end

      it 'sets the correct note text' do
        expect { subject }.to change { Note.system.count }.by(2)
        expect(work_item.notes.last.note).to eq("added ##{task.iid} as child task")
        expect(task.notes.last.note).to eq("added ##{work_item.iid} as parent issue")
      end
    end

    context 'when child task is removed' do
      let(:hierarchy_change_action) { 'unrelate' }

      it_behaves_like 'a system note' do
        let(:expected_noteable) { task }
        let(:action) { 'unrelate_from_parent' }
      end

      it 'sets the correct note text' do
        expect { subject }.to change { Note.system.count }.by(2)
        expect(work_item.notes.last.note).to eq("removed child task ##{task.iid}")
        expect(task.notes.last.note).to eq("removed parent issue ##{work_item.iid}")
      end
    end
  end
end