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

module Ci
  module Pipelines
    class AddJobService
      include ::Gitlab::ExclusiveLeaseHelpers

      attr_reader :pipeline

      def initialize(pipeline)
        @pipeline = pipeline

        raise ArgumentError, "Pipeline must be persisted for this service to be used" unless pipeline.persisted?
      end

      def execute!(job, &block)
        assign_pipeline_attributes(job)

        in_lock("ci:pipelines:#{pipeline.id}:add-job", ttl: LOCK_TIMEOUT, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES) do
          Ci::Pipeline.transaction do
            yield(job)

            job.update_older_statuses_retried!
          end
        end

        ServiceResponse.success(payload: { job: job })
      rescue StandardError => e
        ServiceResponse.error(message: e.message, payload: { job: job })
      end

      private

      LOCK_TIMEOUT = 1.minute
      LOCK_SLEEP = 0.5.seconds
      LOCK_RETRIES = 20

      def assign_pipeline_attributes(job)
        job.pipeline = pipeline
        job.project = pipeline.project
        job.ref = pipeline.ref
        job.partition_id = pipeline.partition_id

        # update metadata since it might have been lazily initialised before this call
        # metadata is present on `Ci::Processable`
        if job.respond_to?(:metadata) && job.metadata
          job.metadata.project = pipeline.project
          job.metadata.partition_id = pipeline.partition_id
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Pipelines::AddJobService, feature_category: :continuous_integration do
  include ExclusiveLeaseHelpers

  let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }

  let(:job) { build(:ci_build) }

  subject(:service) { described_class.new(pipeline) }

  context 'when the pipeline is not persisted' do
    let(:pipeline) { build(:ci_pipeline) }

    it 'raises error' do
      expect { service }.to raise_error('Pipeline must be persisted for this service to be used')
    end
  end

  describe '#execute!' do
    subject(:execute) do
      service.execute!(job) do |job|
        job.save!
      end
    end

    it 'assigns pipeline attributes to the job' do
      expect do
        execute
      end.to change { job.slice(:pipeline, :project, :ref) }.to(
        pipeline: pipeline, project: pipeline.project, ref: pipeline.ref
      ).and change { job.metadata.project }.to(pipeline.project)
    end

    it 'assigns partition_id to job and metadata' do
      pipeline.partition_id = ci_testing_partition_id

      expect { execute }
        .to change(job, :partition_id).to(pipeline.partition_id)
        .and change { job.metadata.partition_id }.to(pipeline.partition_id)
    end

    it 'returns a service response with the job as payload' do
      expect(execute).to be_success
      expect(execute.payload[:job]).to eq(job)
    end

    it 'calls update_older_statuses_retried!' do
      expect(job).to receive(:update_older_statuses_retried!)

      execute
    end

    context 'when the block raises an error' do
      subject(:execute) do
        service.execute!(job) do |job|
          raise "this is an error"
        end
      end

      it 'returns a service response with the error and the job as payload' do
        expect(execute).to be_error
        expect(execute.payload[:job]).to eq(job)
        expect(execute.message).to eq('this is an error')
      end
    end

    context 'exclusive lock' do
      let(:lock_uuid) { 'test' }
      let(:lock_key) { "ci:pipelines:#{pipeline.id}:add-job" }
      let(:lock_timeout) { 1.minute }

      before do
        # "Please stub a default value first if message might be received with other args as well."
        allow(Gitlab::ExclusiveLease).to receive(:new).and_call_original
      end

      it 'uses exclusive lock' do
        lease = stub_exclusive_lease(lock_key, lock_uuid, timeout: lock_timeout)
        expect(lease).to receive(:try_obtain)
        expect(lease).to receive(:cancel)

        expect(execute).to be_success
        expect(execute.payload[:job]).to eq(job)
      end
    end
  end
end


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

module Ci
  module Pipelines
    class HookService
      include Gitlab::Utils::StrongMemoize

      HOOK_NAME = :pipeline_hooks

      def initialize(pipeline)
        @pipeline = pipeline
      end

      def execute
        project.execute_hooks(hook_data, HOOK_NAME) if project.has_active_hooks?(HOOK_NAME)
        project.execute_integrations(hook_data, HOOK_NAME) if project.has_active_integrations?(HOOK_NAME)
      end

      private

      attr_reader :pipeline

      def project
        @project ||= pipeline.project
      end

      def hook_data
        strong_memoize(:hook_data) do
          Gitlab::DataBuilder::Pipeline.build(pipeline)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Pipelines::HookService, feature_category: :continuous_integration do
  describe '#execute_hooks' do
    let_it_be(:namespace) { create(:namespace) }
    let_it_be(:project) { create(:project, :repository, namespace: namespace) }
    let_it_be(:pipeline, reload: true) { create(:ci_empty_pipeline, :created, project: project) }

    let(:hook_enabled) { true }
    let!(:hook) { create(:project_hook, project: project, pipeline_events: hook_enabled) }
    let(:hook_data) { double }

    subject(:service) { described_class.new(pipeline) }

    describe 'HOOK_NAME' do
      specify { expect(described_class::HOOK_NAME).to eq(:pipeline_hooks) }
    end

    context 'with pipeline hooks enabled' do
      before do
        allow(Gitlab::DataBuilder::Pipeline).to receive(:build).with(pipeline).once.and_return(hook_data)
      end

      it 'calls pipeline.project.execute_hooks and pipeline.project.execute_integrations' do
        create(:pipelines_email_integration, project: project)

        expect(pipeline.project).to receive(:execute_hooks).with(hook_data, described_class::HOOK_NAME)
        expect(pipeline.project).to receive(:execute_integrations).with(hook_data, described_class::HOOK_NAME)

        service.execute
      end
    end

    context 'with pipeline hooks and integrations disabled' do
      let(:hook_enabled) { false }

      it 'does not call pipeline.project.execute_hooks and pipeline.project.execute_integrations' do
        expect(pipeline.project).not_to receive(:execute_hooks)
        expect(pipeline.project).not_to receive(:execute_integrations)

        service.execute
      end
    end
  end
end


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

module Ci
  module Pipelines
    class UpdateMetadataService
      def initialize(pipeline, params)
        @pipeline = pipeline
        @params = params
      end

      def execute
        metadata = pipeline.pipeline_metadata

        metadata = pipeline.build_pipeline_metadata(project: pipeline.project) if metadata.nil?

        params[:name] = params[:name].strip if params.key?(:name)

        if metadata.update(params)
          ServiceResponse.success(message: 'Pipeline metadata was updated', payload: pipeline)
        else
          ServiceResponse.error(message: 'Failed to update pipeline', payload: metadata.errors.full_messages,
            reason: :bad_request)
        end
      end

      private

      attr_reader :pipeline, :params
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Pipelines::UpdateMetadataService, feature_category: :continuous_integration do
  subject(:execute) { described_class.new(pipeline, { name: name }).execute }

  let(:name) { 'Some random pipeline name' }

  context 'when pipeline has no name' do
    let(:pipeline) { create(:ci_pipeline) }

    it 'updates the name' do
      expect { execute }.to change { pipeline.reload.name }.to(name)
    end
  end

  context 'when pipeline has a name' do
    let(:pipeline) { create(:ci_pipeline, name: 'Some other name') }

    it 'updates the name' do
      expect { execute }.to change { pipeline.reload.name }.to(name)
    end
  end

  context 'when new name is too long' do
    let(:pipeline) { create(:ci_pipeline) }
    let(:name) { 'a' * 256 }

    it 'does not update the name' do
      expect { execute }.not_to change { pipeline.reload.name }
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true
module Ci
  module PipelineArtifacts
    class CreateCodeQualityMrDiffReportService
      include Gitlab::Utils::StrongMemoize

      def initialize(pipeline)
        @pipeline = pipeline
      end

      def execute
        return unless pipeline.can_generate_codequality_reports?
        return if pipeline.has_codequality_mr_diff_report?
        return unless new_errors_introduced?

        Ci::PipelineArtifact.create_or_replace_for_pipeline!(**artifact_attributes)
      end

      private

      attr_reader :pipeline

      def artifact_attributes
        file = build_carrierwave_file!

        {
          pipeline: pipeline,
          file_type: :code_quality_mr_diff,
          size: file["tempfile"].size,
          file: file,
          locked: pipeline.locked
        }
      end

      def merge_requests
        strong_memoize(:merge_requests) do
          pipeline.merge_requests_as_head_pipeline
        end
      end

      def head_report
        strong_memoize(:head_report) do
          pipeline.codequality_reports
        end
      end

      def base_report(merge_request)
        strong_memoize(:base_report) do
          merge_request&.base_pipeline&.codequality_reports
        end
      end

      def mr_diff_report_by_merge_requests
        strong_memoize(:mr_diff_report_by_merge_requests) do
          merge_requests.each_with_object({}) do |merge_request, hash|
            key = "merge_request_#{merge_request.id}"
            new_errors = Gitlab::Ci::Reports::CodequalityReportsComparer.new(base_report(merge_request), head_report).new_errors
            next if new_errors.empty?

            hash[key] = Gitlab::Ci::Reports::CodequalityMrDiff.new(new_errors)
          end
        end
      end

      def new_errors_introduced?
        mr_diff_report_by_merge_requests.present?
      end

      def build_carrierwave_file!
        CarrierWaveStringFile.new_file(
          file_content: build_quality_mr_diff_report(mr_diff_report_by_merge_requests),
          filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_quality_mr_diff),
          content_type: 'application/json'
        )
      end

      def build_quality_mr_diff_report(mr_diff_report)
        Gitlab::Json.dump(mr_diff_report.each_with_object({}) do |diff_report, hash|
          hash[diff_report.first] = Ci::CodequalityMrDiffReportSerializer.new.represent(diff_report.second) # rubocop: disable CodeReuse/Serializer
        end)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::PipelineArtifacts::CreateCodeQualityMrDiffReportService, feature_category: :build_artifacts do
  describe '#execute' do
    let(:merge_request) { create(:merge_request) }
    let(:project) { merge_request.project }
    let(:head_pipeline) { create(:ci_pipeline, :success, :with_codequality_reports, project: project, merge_requests_as_head_pipeline: [merge_request]) }
    let(:base_pipeline) { create(:ci_pipeline, :success, project: project, ref: merge_request.target_branch, sha: merge_request.diff_base_sha) }

    subject { described_class.new(head_pipeline).execute }

    context 'when there are codequality reports' do
      context 'when pipeline passes' do
        context 'when degradations are present' do
          context 'when degradations already present in target branch pipeline' do
            before do
              create(:ci_build, :success, :codequality_reports, name: 'codequality', pipeline: base_pipeline, project: project)
            end

            it "does not persist a pipeline artifact" do
              expect { subject }.not_to change { Ci::PipelineArtifact.count }
            end
          end

          context 'when degradation is not present in target branch pipeline' do
            before do
              create(:ci_build, :success, :codequality_reports_without_degradation, name: 'codequality', pipeline: base_pipeline, project: project)
            end

            it 'persists a pipeline artifact' do
              expect { subject }.to change { Ci::PipelineArtifact.count }.by(1)
            end

            it 'persists the default file name' do
              subject

              pipeline_artifact = Ci::PipelineArtifact.first

              expect(pipeline_artifact.file.filename).to eq('code_quality_mr_diff.json')
            end

            it 'sets expire_at to 1 week' do
              freeze_time do
                subject

                pipeline_artifact = Ci::PipelineArtifact.first

                expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
              end
            end

            it "artifact has pipeline's locked status" do
              subject

              artifact = Ci::PipelineArtifact.first

              expect(artifact.locked).to eq(head_pipeline.locked)
            end

            it 'does not persist the same artifact twice' do
              2.times { described_class.new(head_pipeline).execute }

              expect { subject }.not_to change { Ci::PipelineArtifact.count }
            end
          end
        end
      end
    end

    context 'when there are no codequality reports for head pipeline' do
      let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) }

      it "does not persist a pipeline artifact" do
        expect { subject }.not_to change { Ci::PipelineArtifact.count }
      end
    end

    context 'when there are no codequality reports for base pipeline' do
      let(:head_pipeline) { create(:ci_pipeline, :success, project: project, merge_requests_as_head_pipeline: [merge_request]) }

      it "does not persist a pipeline artifact" do
        expect { subject }.not_to change { Ci::PipelineArtifact.count }
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
# frozen_string_literal: true
module Ci
  module PipelineArtifacts
    class CoverageReportService
      include Gitlab::Utils::StrongMemoize

      def initialize(pipeline)
        @pipeline = pipeline
      end

      def execute
        return if report.empty?

        Ci::PipelineArtifact.create_or_replace_for_pipeline!(**pipeline_artifact_params).tap do |pipeline_artifact|
          Gitlab::AppLogger.info(log_params(pipeline_artifact))
        end
      end

      private

      attr_reader :pipeline

      def report
        strong_memoize(:report) do
          Gitlab::Ci::Reports::CoverageReportGenerator.new(pipeline).report
        end
      end

      def pipeline_artifact_params
        {
          pipeline: pipeline,
          file_type: :code_coverage,
          file: carrierwave_file,
          size: carrierwave_file['tempfile'].size,
          locked: pipeline.locked
        }
      end

      def carrierwave_file
        strong_memoize(:carrier_wave_file) do
          CarrierWaveStringFile.new_file(
            file_content: Gitlab::Json.dump(report),
            filename: Ci::PipelineArtifact::DEFAULT_FILE_NAMES.fetch(:code_coverage),
            content_type: 'application/json'
          )
        end
      end

      def log_params(pipeline_artifact)
        {
          project_id: pipeline.project_id,
          pipeline_id: pipeline.id,
          pipeline_artifact_id: pipeline_artifact.id,
          message: "Created code coverage for pipeline."
        }
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineArtifacts::CoverageReportService, feature_category: :build_artifacts do
  describe '#execute' do
    let_it_be(:project) { create(:project, :repository) }

    subject { described_class.new(pipeline).execute }

    shared_examples 'creating or updating a pipeline coverage report' do
      context 'when pipeline is finished' do
        it 'creates or updates a pipeline artifact' do
          subject

          expect(pipeline.reload.pipeline_artifacts.count).to eq(1)
        end

        it 'persists the default file name' do
          subject

          file = Ci::PipelineArtifact.first.file

          expect(file.filename).to eq('code_coverage.json')
        end

        it 'sets expire_at to 1 week from now' do
          freeze_time do
            subject

            pipeline_artifact = Ci::PipelineArtifact.first

            expect(pipeline_artifact.expire_at).to eq(1.week.from_now)
          end
        end

        it 'logs relevant information' do
          allow(Gitlab::AppLogger).to receive(:info).and_call_original
          expect(Gitlab::AppLogger).to receive(:info).with({
                                                             project_id: project.id,
                                                             pipeline_id: pipeline.id,
                                                             pipeline_artifact_id: kind_of(Numeric),
                                                             message: kind_of(String)
                                                           })

          subject
        end
      end
    end

    context 'when pipeline has coverage report' do
      let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }

      it_behaves_like 'creating or updating a pipeline coverage report'

      it "artifact has pipeline's locked status" do
        subject

        artifact = Ci::PipelineArtifact.first

        expect(artifact.locked).to eq(pipeline.locked)
      end
    end

    context 'when pipeline has coverage report from child pipeline' do
      let!(:pipeline) { create(:ci_pipeline, :success, project: project) }
      let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) }

      it_behaves_like 'creating or updating a pipeline coverage report'
    end

    context 'when pipeline has existing pipeline artifact for coverage report' do
      let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) }
      let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) }

      let!(:pipeline_artifact) do
        create(:ci_pipeline_artifact, :with_coverage_report, pipeline: pipeline, expire_at: 1.day.from_now)
      end

      it_behaves_like 'creating or updating a pipeline coverage report'
    end

    context 'when pipeline is running and coverage report does not exist' do
      let(:pipeline) { create(:ci_pipeline, :running) }

      it 'does not persist data' do
        expect { subject }.not_to change { Ci::PipelineArtifact.count }.from(0)
      end
    end
  end
end


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

module Ci
  module PipelineArtifacts
    class DestroyAllExpiredService
      include ::Gitlab::ExclusiveLeaseHelpers
      include ::Gitlab::LoopHelpers
      include ::Gitlab::Utils::StrongMemoize

      BATCH_SIZE = 100
      LOOP_LIMIT = 1000
      LOOP_TIMEOUT = 5.minutes
      LOCK_TIMEOUT = 10.minutes
      EXCLUSIVE_LOCK_KEY = 'expired_pipeline_artifacts:destroy:lock'

      def initialize
        @removed_artifacts_count = 0
        @start_at = Time.current
      end

      def execute
        in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
          destroy_unlocked_pipeline_artifacts

          legacy_destroy_pipeline_artifacts
        end

        @removed_artifacts_count
      end

      private

      def destroy_unlocked_pipeline_artifacts
        loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
          artifacts = Ci::PipelineArtifact.expired_before(@start_at).artifact_unlocked.limit(BATCH_SIZE)

          break if artifacts.empty?

          destroy_batch(artifacts)
        end
      end

      def legacy_destroy_pipeline_artifacts
        loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
          destroy_artifacts_batch
        end
      end

      def destroy_artifacts_batch
        artifacts = ::Ci::PipelineArtifact.unlocked.expired.limit(BATCH_SIZE).to_a
        return false if artifacts.empty?

        destroy_batch(artifacts)
      end

      def destroy_batch(artifacts)
        artifacts.each(&:destroy!)
        increment_stats(artifacts.size)

        true
      end

      def increment_stats(size)
        destroyed_artifacts_counter.increment({}, size)
        @removed_artifacts_count += size
      end

      def destroyed_artifacts_counter
        strong_memoize(:destroyed_artifacts_counter) do
          name = :destroyed_pipeline_artifacts_count_total
          comment = 'Counter of destroyed expired pipeline artifacts'

          ::Gitlab::Metrics.counter(name, comment)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state,
  feature_category: :build_artifacts do
  let(:service) { described_class.new }

  describe '.execute' do
    subject { service.execute }

    context 'when timeout happens' do
      before do
        stub_const('Ci::PipelineArtifacts::DestroyAllExpiredService::LOOP_TIMEOUT', 0.1.seconds)
        allow(service).to receive(:destroy_artifacts_batch) { true }
      end

      it 'returns 0 and does not continue destroying' do
        is_expected.to eq(0)
      end
    end

    context 'when there are no artifacts' do
      it 'does not raise error' do
        expect { subject }.not_to raise_error
      end
    end

    context 'when the loop limit is reached' do
      before do
        stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::LOOP_LIMIT', 1)
        stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1)

        create_list(:ci_pipeline_artifact, 2, :unlocked, expire_at: 1.week.ago)
      end

      it 'destroys one artifact' do
        expect { subject }.to change { Ci::PipelineArtifact.count }.by(-1)
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(1)
      end
    end

    context 'when there are artifacts more than batch sizes' do
      before do
        stub_const('Ci::PipelineArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1)

        create_list(:ci_pipeline_artifact, 2, :unlocked, expire_at: 1.week.ago)
      end

      it 'destroys all expired artifacts' do
        expect { subject }.to change { Ci::PipelineArtifact.count }.by(-2)
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(2)
      end
    end

    context 'when artifacts are not expired' do
      before do
        create(:ci_pipeline_artifact, :unlocked, expire_at: 2.days.from_now)
      end

      it 'does not destroy pipeline artifacts' do
        expect { subject }.not_to change { Ci::PipelineArtifact.count }
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(0)
      end
    end

    context 'when pipeline is locked' do
      before do
        create(:ci_pipeline_artifact, expire_at: 2.weeks.ago)
      end

      it 'does not destroy pipeline artifacts' do
        expect { subject }.not_to change { Ci::PipelineArtifact.count }
      end

      it 'reports the number of destroyed artifacts' do
        is_expected.to eq(0)
      end
    end

    context 'with unlocked pipeline artifacts' do
      let_it_be(:not_expired_artifact) { create(:ci_pipeline_artifact, :artifact_unlocked, expire_at: 2.days.from_now) }

      before do
        create_list(:ci_pipeline_artifact, 2, :artifact_unlocked, expire_at: 1.week.ago)
        allow(service).to receive(:legacy_destroy_pipeline_artifacts)
      end

      it 'destroys all expired artifacts' do
        expect { subject }.to change { Ci::PipelineArtifact.count }.by(-2)
        expect(not_expired_artifact.reload).to be_present
      end

      context 'when the loop limit is reached' do
        before do
          stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::LOOP_LIMIT', 1)
          stub_const('::Ci::PipelineArtifacts::DestroyAllExpiredService::BATCH_SIZE', 1)
        end

        it 'destroys one artifact' do
          expect { subject }.to change { Ci::PipelineArtifact.count }.by(-1)
          expect(not_expired_artifact.reload).to be_present
        end

        it 'reports the number of destroyed artifacts' do
          is_expected.to eq(1)
        end
      end
    end
  end

  describe '.destroy_artifacts_batch' do
    it 'returns a falsy value without artifacts' do
      expect(service.send(:destroy_artifacts_batch)).to be_falsy
    end
  end
end


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

# This service is responsible for creating a pipeline for a given
# ExternalPullRequest coming from other providers such as GitHub.

module Ci
  module ExternalPullRequests
    class CreatePipelineService < BaseService
      def execute(pull_request)
        return pull_request_not_open_error unless pull_request.open?
        return pull_request_branch_error unless pull_request.actual_branch_head?

        Ci::ExternalPullRequests::CreatePipelineWorker.perform_async(
          project.id, current_user.id, pull_request.id
        )
      end

      private

      def pull_request_not_open_error
        ServiceResponse.error(message: 'The pull request is not opened', payload: nil)
      end

      def pull_request_branch_error
        ServiceResponse.error(message: 'The source sha is not the head of the source branch', payload: nil)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::ExternalPullRequests::CreatePipelineService, feature_category: :continuous_integration do
  describe '#execute' do
    let_it_be(:project) { create(:project, :auto_devops, :repository) }
    let_it_be(:user) { create(:user) }
    let_it_be_with_reload(:pull_request) { create(:external_pull_request, project: project) }

    before do
      project.add_maintainer(user)
    end

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

    context 'when pull request is open' do
      before do
        pull_request.update!(status: :open)
      end

      context 'when source sha is the head of the source branch' do
        let(:source_branch) { project.repository.branches.last }

        before do
          pull_request.update!(source_branch: source_branch.name, source_sha: source_branch.target)
        end

        it 'enqueues Ci::ExternalPullRequests::CreatePipelineWorker' do
          expect { execute }
            .to change { ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.count }
            .by(1)

          args = ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.last['args']

          expect(args[0]).to eq(project.id)
          expect(args[1]).to eq(user.id)
          expect(args[2]).to eq(pull_request.id)
        end
      end

      context 'when source sha is not the head of the source branch (force push upon rebase)' do
        let(:source_branch) { project.repository.branches.first }
        let(:commit) { project.repository.commits(source_branch.name, limit: 2).last }

        before do
          pull_request.update!(source_branch: source_branch.name, source_sha: commit.sha)
        end

        it 'does nothing', :aggregate_failures do
          expect { execute }
            .not_to change { ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.count }

          expect(execute).to be_error
          expect(execute.message).to eq('The source sha is not the head of the source branch')
          expect(execute.payload).to be_nil
        end
      end
    end

    context 'when pull request is not opened' do
      before do
        pull_request.update!(status: :closed)
      end

      it 'does nothing', :aggregate_failures do
        expect { execute }
          .not_to change { ::Ci::ExternalPullRequests::CreatePipelineWorker.jobs.count }

        expect(execute).to be_error
        expect(execute.message).to eq('The pull request is not opened')
        expect(execute.payload).to be_nil
      end
    end
  end
end


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

module Ci
  module Components
    class FetchService
      include Gitlab::Utils::StrongMemoize

      COMPONENT_PATHS = [
        ::Gitlab::Ci::Components::InstancePath
      ].freeze

      def initialize(address:, current_user:)
        @address = address
        @current_user = current_user
      end

      def execute
        unless component_path_class
          return ServiceResponse.error(
            message: "#{error_prefix} the component path is not supported",
            reason: :unsupported_path)
        end

        component_path = component_path_class.new(address: address)
        result = component_path.fetch_content!(current_user: current_user)

        if result
          ServiceResponse.success(payload: {
            content: result.content,
            path: result.path,
            project: component_path.project,
            sha: component_path.sha
          })
        else
          ServiceResponse.error(message: "#{error_prefix} content not found", reason: :content_not_found)
        end
      rescue Gitlab::Access::AccessDeniedError
        ServiceResponse.error(
          message: "#{error_prefix} project does not exist or you don't have sufficient permissions",
          reason: :not_allowed)
      end

      private

      attr_reader :current_user, :address

      def component_path_class
        COMPONENT_PATHS.find { |klass| klass.match?(address) }
      end
      strong_memoize_attr :component_path_class

      def error_prefix
        "component '#{address}' -"
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composition do
  let_it_be(:user) { create(:user) }
  let_it_be(:current_user) { user }
  let_it_be(:current_host) { Gitlab.config.gitlab.host }
  let_it_be(:content) do
    <<~COMPONENT
    job:
      script: echo
    COMPONENT
  end

  let(:service) do
    described_class.new(address: address, current_user: current_user)
  end

  let_it_be(:project) do
    project = create(
      :project, :custom_repo,
      files: {
        'template.yml' => content,
        'my-component/template.yml' => content,
        'my-dir/my-component/template.yml' => content
      }
    )

    project.repository.add_tag(project.creator, 'v0.1', project.repository.commit.sha)

    project
  end

  before do
    project.add_developer(user)
  end

  describe '#execute', :aggregate_failures do
    subject(:result) { service.execute }

    shared_examples 'an external component' do
      shared_examples 'component address' do
        context 'when content exists' do
          it 'returns the content' do
            expect(result).to be_success
            expect(result.payload[:content]).to eq(content)
          end
        end

        context 'when content does not exist' do
          let(:address) { "#{current_host}/#{component_path}@~version-does-not-exist" }

          it 'returns an error' do
            expect(result).to be_error
            expect(result.reason).to eq(:content_not_found)
          end
        end
      end

      context 'when user does not have permissions to read the code' do
        let(:version) { 'master' }
        let(:current_user) { create(:user) }

        it 'returns an error' do
          expect(result).to be_error
          expect(result.reason).to eq(:not_allowed)
        end
      end

      context 'when version is a branch name' do
        it_behaves_like 'component address' do
          let(:version) { project.default_branch }
        end
      end

      context 'when version is a tag name' do
        it_behaves_like 'component address' do
          let(:version) { project.repository.tags.first.name }
        end
      end

      context 'when version is a commit sha' do
        it_behaves_like 'component address' do
          let(:version) { project.repository.tags.first.id }
        end
      end

      context 'when version is not provided' do
        let(:version) { nil }

        it 'returns an error' do
          expect(result).to be_error
          expect(result.reason).to eq(:content_not_found)
        end
      end

      context 'when project does not exist' do
        let(:component_path) { 'unknown/component' }
        let(:version) { '1.0' }

        it 'returns an error' do
          expect(result).to be_error
          expect(result.reason).to eq(:content_not_found)
        end
      end

      context 'when host is different than the current instance host' do
        let(:current_host) { 'another-host.com' }
        let(:version) { '1.0' }

        it 'returns an error' do
          expect(result).to be_error
          expect(result.reason).to eq(:unsupported_path)
        end
      end
    end

    context 'when address points to an external component' do
      let(:address) { "#{current_host}/#{component_path}@#{version}" }

      context 'when component path is the full path to a project' do
        let(:component_path) { project.full_path }
        let(:component_yaml_path) { 'template.yml' }

        it_behaves_like 'an external component'
      end

      context 'when component path points to a directory in a project' do
        let(:component_path) { "#{project.full_path}/my-component" }
        let(:component_yaml_path) { 'my-component/template.yml' }

        it_behaves_like 'an external component'
      end

      context 'when component path points to a nested directory in a project' do
        let(:component_path) { "#{project.full_path}/my-dir/my-component" }
        let(:component_yaml_path) { 'my-dir/my-component/template.yml' }

        it_behaves_like 'an external component'
      end
    end
  end

  def stub_project_blob(ref, path, content)
    allow_next_instance_of(Repository) do |instance|
      allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content)
    end
  end
end


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

module Ci
  module PrometheusMetrics
    class ObserveHistogramsService
      class << self
        def available_histograms
          @available_histograms ||= [
            histogram(:pipeline_graph_link_calculation_duration_seconds, 'Total time spent calculating links, in seconds', {}, [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.8, 1, 2]),
            histogram(:pipeline_graph_links_total, 'Number of links per graph', {}, [1, 5, 10, 25, 50, 100, 200]),
            histogram(:pipeline_graph_links_per_job_ratio, 'Ratio of links to job per graph', {}, [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1])
          ].to_h
        end

        private

        def histogram(name, *attrs)
          [name.to_s, proc { Gitlab::Metrics.histogram(name, *attrs) }]
        end
      end

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

      def execute
        params
          .fetch(:histograms, [])
          .each { |data| observe(data) }

        ServiceResponse.success(http_status: :created)
      end

      private

      attr_reader :project, :params

      def observe(data)
        histogram = find_histogram(data[:name])
        histogram.observe({}, data[:value].to_f)
      end

      def find_histogram(name)
        self.class.available_histograms
          .fetch(name) { raise ActiveRecord::RecordNotFound }
          .call
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PrometheusMetrics::ObserveHistogramsService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project) }

  let(:params) { {} }

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

  before do
    Gitlab::Metrics.reset_registry!
  end

  context 'with empty data' do
    it 'does not raise errors' do
      is_expected.to be_success
    end
  end

  context 'observes metrics successfully' do
    let(:params) do
      {
        histograms: [
          { name: 'pipeline_graph_link_calculation_duration_seconds', value: '1' },
          { name: 'pipeline_graph_links_per_job_ratio', value: '0.9' }
        ]
      }
    end

    it 'increments the metrics' do
      execute

      expect(histogram_data).to match(a_hash_including({ 0.8 => 0.0, 1 => 1.0, 2 => 1.0 }))

      expect(histogram_data(:pipeline_graph_links_per_job_ratio))
        .to match(a_hash_including({ 0.8 => 0.0, 0.9 => 1.0, 1 => 1.0 }))
    end

    it 'returns an empty body and status code' do
      is_expected.to be_success
      expect(subject.http_status).to eq(:created)
      expect(subject.payload).to eq({})
    end
  end

  context 'with unknown histograms' do
    let(:params) do
      { histograms: [{ name: 'chunky_bacon', value: '4' }] }
    end

    it 'raises ActiveRecord::RecordNotFound error' do
      expect { subject }.to raise_error ActiveRecord::RecordNotFound
    end
  end

  def histogram_data(name = :pipeline_graph_link_calculation_duration_seconds)
    Gitlab::Metrics.registry.get(name)&.get({})
  end
end


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

module Ci
  module Refs
    class EnqueuePipelinesToUnlockService
      include BaseServiceUtility

      BATCH_SIZE = 50
      ENQUEUE_INTERVAL_SECONDS = 0.1
      EXCLUDED_IDS_LIMIT = 1000

      def execute(ci_ref, before_pipeline: nil)
        total_new_entries = 0

        pipelines_scope(ci_ref, before_pipeline).each_batch(of: BATCH_SIZE) do |batch|
          pipeline_ids = batch.pluck(:id) # rubocop: disable CodeReuse/ActiveRecord
          total_added = Ci::UnlockPipelineRequest.enqueue(pipeline_ids)
          total_new_entries += total_added

          # Take a little rest to avoid overloading Redis
          sleep ENQUEUE_INTERVAL_SECONDS
        end

        success(
          total_pending_entries: Ci::UnlockPipelineRequest.total_pending,
          total_new_entries: total_new_entries
        )
      end

      private

      def pipelines_scope(ci_ref, before_pipeline)
        scope = ci_ref.pipelines.artifacts_locked

        if before_pipeline
          # We use `same_family_pipeline_ids.map(&:id)` to force run the query and
          # specifically pass the array of IDs to the NOT IN condition. If not, we would
          # end up running the subquery for same_family_pipeline_ids on each batch instead.
          excluded_ids = before_pipeline.same_family_pipeline_ids.map(&:id)
          scope = scope.created_before_id(before_pipeline.id)

          # When unlocking previous pipelines, we still want to keep the
          # last successful CI source pipeline locked.
          # If before_pipeline is not provided, like in the case of deleting a ref,
          # we want to unlock all pipelines instead.
          ci_ref.last_successful_ci_source_pipeline.try do |pipeline|
            excluded_ids.concat(pipeline.same_family_pipeline_ids.map(&:id))
          end

          # We add a limit to the excluded IDs just to be safe and avoid any
          # arity issues with the NOT IN query.
          scope = scope.where.not(id: excluded_ids.take(EXCLUDED_IDS_LIMIT)) # rubocop: disable CodeReuse/ActiveRecord
        end

        scope
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Refs::EnqueuePipelinesToUnlockService, :unlock_pipelines, :clean_gitlab_redis_shared_state, feature_category: :build_artifacts do
  describe '#execute' do
    let_it_be(:ref) { 'master' }
    let_it_be(:project) { create(:project) }
    let_it_be(:tag_ref_path) { "#{::Gitlab::Git::TAG_REF_PREFIX}#{ref}" }
    let_it_be(:ci_ref_tag) { create(:ci_ref, ref_path: tag_ref_path, project: project) }
    let_it_be(:branch_ref_path) { "#{::Gitlab::Git::BRANCH_REF_PREFIX}#{ref}" }
    let_it_be(:ci_ref_branch) { create(:ci_ref, ref_path: branch_ref_path, project: project) }
    let_it_be(:other_ref) { 'other_ref' }
    let_it_be(:other_ref_path) { "#{::Gitlab::Git::BRANCH_REF_PREFIX}#{other_ref}" }
    let_it_be(:other_ci_ref) { create(:ci_ref, ref_path: other_ref_path, project: project) }

    let(:service) { described_class.new }

    subject(:execute) { service.execute(target_ref, before_pipeline: before_pipeline) }

    before do
      stub_const("#{described_class}::BATCH_SIZE", 2)
      stub_const("#{described_class}::ENQUEUE_INTERVAL_SECONDS", 0)
    end

    shared_examples_for 'unlocking pipelines' do
      let(:is_tag) { target_ref.ref_path.include?(::Gitlab::Git::TAG_REF_PREFIX) }

      let!(:other_ref_pipeline) { create_pipeline(:locked, other_ref, :failed, tag: false) }
      let!(:old_unlocked_pipeline) { create_pipeline(:unlocked, ref, :failed) }
      let!(:old_locked_pipeline_1) { create_pipeline(:locked, ref, :failed) }
      let!(:old_locked_pipeline_2) { create_pipeline(:locked, ref, :success) }
      let!(:old_locked_pipeline_3) { create_pipeline(:locked, ref, :success) }
      let!(:old_locked_pipeline_3_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_3) }
      let!(:old_locked_pipeline_4) { create_pipeline(:locked, ref, :success) }
      let!(:old_locked_pipeline_4_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_4) }
      let!(:old_locked_pipeline_5) { create_pipeline(:locked, ref, :failed) }
      let!(:old_locked_pipeline_5_child) { create_pipeline(:locked, ref, :success, child_of: old_locked_pipeline_5) }
      let!(:pipeline) { create_pipeline(:locked, ref, :failed) }
      let!(:child_pipeline) { create_pipeline(:locked, ref, :failed, child_of: pipeline) }
      let!(:newer_pipeline) { create_pipeline(:locked, ref, :failed) }

      context 'when before_pipeline is given' do
        let(:before_pipeline) { pipeline }

        it 'only enqueues old locked pipelines within the ref, excluding the last successful CI source pipeline' do
          expect { execute }
            .to change { pipeline_ids_waiting_to_be_unlocked }
            .from([])
            .to([
              old_locked_pipeline_1.id,
              old_locked_pipeline_2.id,
              old_locked_pipeline_3.id,
              old_locked_pipeline_3_child.id,
              old_locked_pipeline_5.id,
              old_locked_pipeline_5_child.id
            ])

          expect(execute).to include(
            status: :success,
            total_pending_entries: 6,
            total_new_entries: 6
          )
        end
      end

      context 'when before_pipeline is not given' do
        let(:before_pipeline) { nil }

        it 'enqueues all locked pipelines within the ref' do
          expect { execute }
            .to change { pipeline_ids_waiting_to_be_unlocked }
            .from([])
            .to([
              old_locked_pipeline_1.id,
              old_locked_pipeline_2.id,
              old_locked_pipeline_3.id,
              old_locked_pipeline_3_child.id,
              old_locked_pipeline_4.id,
              old_locked_pipeline_4_child.id,
              old_locked_pipeline_5.id,
              old_locked_pipeline_5_child.id,
              pipeline.id,
              child_pipeline.id,
              newer_pipeline.id
            ])

          expect(execute).to include(
            status: :success,
            total_pending_entries: 11,
            total_new_entries: 11
          )
        end
      end
    end

    context 'when ref is a tag' do
      let(:target_ref) { ci_ref_tag }

      it_behaves_like 'unlocking pipelines'
    end

    context 'when ref is a branch' do
      let(:target_ref) { ci_ref_branch }

      it_behaves_like 'unlocking pipelines'
    end

    def create_pipeline(type, ref, status, tag: is_tag, child_of: nil)
      trait = type == :locked ? :artifacts_locked : :unlocked
      create(:ci_pipeline, trait, status: status, ref: ref, tag: tag, project: project, child_of: child_of).tap do |p|
        if child_of
          build = create(:ci_build, pipeline: child_of)
          create(:ci_sources_pipeline, source_job: build, source_project: project, pipeline: p, project: project)
        end
      end
    end
  end
end


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

module Ci
  module Queue
    class PendingBuildsStrategy
      attr_reader :runner

      def initialize(runner)
        @runner = runner
      end

      # rubocop:disable CodeReuse/ActiveRecord
      def builds_for_shared_runner
        shared_builds = builds_available_for_shared_runners

        builds_ordered_for_shared_runners(shared_builds)
      end

      def builds_for_group_runner
        return new_builds.none if runner.namespace_ids.empty?

        new_builds_relation = new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids)

        return order(new_builds_relation) if ::Feature.enabled?(:order_builds_for_group_runner)

        new_builds_relation
      end

      def builds_matching_tag_ids(relation, ids)
        relation.for_tags(runner.tags_ids)
      end

      def builds_with_any_tags(relation)
        relation.where('cardinality(tag_ids) > 0')
      end

      def order(relation)
        relation.order('build_id ASC')
      end

      def new_builds
        ::Ci::PendingBuild.all
      end

      def build_ids(relation)
        relation.pluck(:build_id)
      end

      private

      def builds_available_for_shared_runners
        new_builds.with_instance_runners
      end

      def builds_ordered_for_shared_runners(relation)
        if Feature.enabled?(:ci_queueing_disaster_recovery_disable_fair_scheduling, runner, type: :ops)
          # if disaster recovery is enabled, we fallback to FIFO scheduling
          relation.order('ci_pending_builds.build_id ASC')
        else
          # Implements Fair Scheduling
          # Builds are ordered by projects that have the fewest running builds.
          # This keeps projects that create many builds at once from hogging capacity but
          # has the downside of penalizing projects with lots of builds created in a short period of time
          relation
            .with(running_builds_for_shared_runners_cte.to_arel)
            .joins("LEFT JOIN project_builds ON ci_pending_builds.project_id = project_builds.project_id")
            .order(Arel.sql('COALESCE(project_builds.running_builds, 0) ASC'), 'ci_pending_builds.build_id ASC')
        end
      end

      def running_builds_for_shared_runners_cte
        running_builds = ::Ci::RunningBuild
          .instance_type
          .group(:project_id)
          .select(:project_id, 'COUNT(*) AS running_builds')

        ::Gitlab::SQL::CTE
          .new(:project_builds, running_builds, materialized: true)
      end
      # rubocop:enable CodeReuse/ActiveRecord
    end
  end
end

Ci::Queue::PendingBuildsStrategy.prepend_mod_with('Ci::Queue::PendingBuildsStrategy')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Queue::PendingBuildsStrategy, feature_category: :continuous_integration do
  let_it_be(:group) { create(:group) }
  let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
  let_it_be(:project) { create(:project, group: group) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  let!(:build_1) { create(:ci_build, :created, pipeline: pipeline) }
  let!(:build_2) { create(:ci_build, :created, pipeline: pipeline) }
  let!(:build_3) { create(:ci_build, :created, pipeline: pipeline) }
  let!(:pending_build_1) { create(:ci_pending_build, build: build_2, project: project) }
  let!(:pending_build_2) { create(:ci_pending_build, build: build_3, project: project) }
  let!(:pending_build_3) { create(:ci_pending_build, build: build_1, project: project) }

  describe 'builds_for_group_runner' do
    it 'returns builds ordered by build ID' do
      strategy = described_class.new(group_runner)
      expect(strategy.builds_for_group_runner).to eq([pending_build_3, pending_build_1, pending_build_2])
    end
  end
end


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

module Ci
  module Runners
    class UpdateRunnerService
      attr_reader :runner

      def initialize(runner)
        @runner = runner
      end

      def execute(params)
        params[:active] = !params.delete(:paused) if params.include?(:paused)

        if runner.update(params)
          runner.tick_runner_queue
          ServiceResponse.success
        else
          ServiceResponse.error(message: runner.errors.full_messages)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::Runners::UpdateRunnerService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(runner).execute(params) }

  let(:runner) { create(:ci_runner) }

  before do
    allow(runner).to receive(:tick_runner_queue)
  end

  context 'with description params' do
    let(:params) { { description: 'new runner' } }

    it 'updates the runner and ticking the queue' do
      expect(execute).to be_success

      runner.reload

      expect(runner).to have_received(:tick_runner_queue)
      expect(runner.description).to eq('new runner')
    end
  end

  context 'with paused param' do
    let(:params) { { paused: true } }

    it 'updates the runner and ticking the queue' do
      expect(runner.active).to be_truthy
      expect(execute).to be_success

      runner.reload

      expect(runner).to have_received(:tick_runner_queue)
      expect(runner.active).to be_falsey
    end
  end

  context 'with cost factor params' do
    let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } }

    it 'updates the runner cost factors' do
      expect(execute).to be_success

      runner.reload

      expect(runner.public_projects_minutes_cost_factor).to eq(1.1)
      expect(runner.private_projects_minutes_cost_factor).to eq(2.2)
    end
  end

  context 'when params are not valid' do
    let(:params) { { run_untagged: false } }

    it 'does not update and returns error because it is not valid' do
      expect(execute).to be_error

      runner.reload

      expect(runner).not_to have_received(:tick_runner_queue)
      expect(runner.run_untagged).to be_truthy
    end
  end
end


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

module Ci
  module Runners
    class SetRunnerAssociatedProjectsService
      # @param [Ci::Runner] runner: the project runner to assign/unassign projects from
      # @param [User] current_user: the user performing the operation
      # @param [Array<Integer>] project_ids: the IDs of the associated projects to assign the runner to
      def initialize(runner:, current_user:, project_ids:)
        @runner = runner
        @current_user = current_user
        @project_ids = project_ids
      end

      def execute
        unless current_user&.can?(:assign_runner, runner)
          return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden)
        end

        return ServiceResponse.success if project_ids.nil?

        set_associated_projects
      end

      private

      def set_associated_projects
        new_project_ids = [runner.owner_project.id] + project_ids

        response = ServiceResponse.success
        runner.transaction do
          # rubocop:disable CodeReuse/ActiveRecord
          current_project_ids = runner.projects.ids
          # rubocop:enable CodeReuse/ActiveRecord

          response = associate_new_projects(new_project_ids, current_project_ids)
          response = disassociate_old_projects(new_project_ids, current_project_ids) if response.success?
          raise ActiveRecord::Rollback, response.errors unless response.success?
        end

        response
      end

      def associate_new_projects(new_project_ids, current_project_ids)
        missing_projects = Project.id_in(new_project_ids - current_project_ids)

        unless missing_projects.all? { |project| current_user.can?(:register_project_runners, project) }
          return ServiceResponse.error(message: 'user is not authorized to add runners to project')
        end

        unless missing_projects.all? { |project| runner.assign_to(project, current_user) }
          return ServiceResponse.error(message: 'failed to assign projects to runner')
        end

        ServiceResponse.success
      end

      def disassociate_old_projects(new_project_ids, current_project_ids)
        projects_to_be_deleted = current_project_ids - new_project_ids
        return ServiceResponse.success if projects_to_be_deleted.empty?

        all_destroyed =
          Ci::RunnerProject
            .destroy_by(project_id: projects_to_be_deleted)
            .all?(&:destroyed?)
        return ServiceResponse.success if all_destroyed

        ServiceResponse.error(message: 'failed to destroy runner project')
      end

      attr_reader :runner, :current_user, :project_ids
    end
  end
end

Ci::Runners::SetRunnerAssociatedProjectsService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', feature_category: :runner_fleet do
  subject(:execute) do
    described_class.new(runner: runner, current_user: user, project_ids: new_projects.map(&:id)).execute
  end

  let_it_be(:owner_project) { create(:project) }
  let_it_be(:project2) { create(:project) }

  let(:original_projects) { [owner_project, project2] }
  let(:runner) { create(:ci_runner, :project, projects: original_projects) }

  context 'without user' do
    let(:user) { nil }
    let(:new_projects) { [project2] }

    it 'does not call assign_to on runner and returns error response', :aggregate_failures do
      expect(runner).not_to receive(:assign_to)

      expect(execute).to be_error
      expect(execute.message).to eq('user not allowed to assign runner')
    end
  end

  context 'with unauthorized user' do
    let(:user) { create(:user) }
    let(:new_projects) { [project2] }

    it 'does not call assign_to on runner and returns error message' do
      expect(runner).not_to receive(:assign_to)

      expect(execute).to be_error
      expect(execute.message).to eq('user not allowed to assign runner')
    end
  end

  context 'with authorized user' do
    let_it_be(:project3) { create(:project) }
    let_it_be(:project4) { create(:project) }

    let(:projects_with_maintainer_access) { original_projects }

    before do
      projects_with_maintainer_access.each { |project| project.add_maintainer(user) }
    end

    shared_context 'with successful requests' do
      context 'when disassociating a project' do
        let(:new_projects) { [project3, project4] }

        it 'reassigns associated projects and returns success response' do
          expect(execute).to be_success

          runner.reload

          expect(runner.owner_project).to eq(owner_project)
          expect(runner.projects.ids).to match_array([owner_project.id] + new_projects.map(&:id))
        end
      end

      context 'when disassociating no projects' do
        let(:new_projects) { [project2, project3] }

        it 'reassigns associated projects and returns success response' do
          expect(execute).to be_success

          runner.reload

          expect(runner.owner_project).to eq(owner_project)
          expect(runner.projects.ids).to match_array([owner_project.id] + new_projects.map(&:id))
        end
      end

      context 'when disassociating all projects' do
        let(:new_projects) { [] }

        it 'reassigns associated projects and returns success response' do
          expect(execute).to be_success

          runner.reload

          expect(runner.owner_project).to eq(owner_project)
          expect(runner.projects.ids).to contain_exactly(owner_project.id)
        end
      end
    end

    shared_context 'with failing destroy calls' do
      let(:new_projects) { [project3, project4] }

      it 'returns error response and rolls back transaction' do
        allow_next_found_instance_of(Ci::RunnerProject) do |runner_project|
          allow(runner_project).to receive(:destroy).and_return(false)
        end

        expect(execute).to be_error
        expect(runner.reload.projects.order(:id)).to eq(original_projects)
      end
    end

    context 'with maintainer user' do
      let(:user) { create(:user) }
      let(:projects_with_maintainer_access) { original_projects + new_projects }

      it_behaves_like 'with successful requests'
      it_behaves_like 'with failing destroy calls'

      context 'when associating new projects' do
        let(:new_projects) { [project3, project4] }

        context 'with missing permissions on one of the new projects' do
          let(:projects_with_maintainer_access) { original_projects + [project3] }

          it 'returns error response and rolls back transaction' do
            expect(execute).to be_error
            expect(execute.errors).to contain_exactly('user is not authorized to add runners to project')
            expect(runner.reload.projects.order(:id)).to eq(original_projects)
          end
        end
      end
    end

    context 'with admin user', :enable_admin_mode do
      let(:user) { create(:user, :admin) }
      let(:projects_with_maintainer_access) { original_projects + new_projects }

      it_behaves_like 'with successful requests'
      it_behaves_like 'with failing destroy calls'
    end
  end
end


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

module Ci
  module Runners
    class BulkDeleteRunnersService
      attr_reader :runners

      RUNNER_LIMIT = 50

      # @param runners [Array<Ci::Runner>] the runners to unregister/destroy
      # @param current_user [User] the user performing the operation
      def initialize(runners:, current_user:)
        @runners = runners
        @current_user = current_user
      end

      def execute
        if @runners
          # Delete a few runners immediately
          return delete_runners
        end

        ServiceResponse.success(payload: { deleted_count: 0, deleted_ids: [], errors: [] })
      end

      private

      def delete_runners
        runner_count = @runners.limit(RUNNER_LIMIT + 1).count
        authorized_runners_ids, unauthorized_runners_ids = compute_authorized_runners
        # rubocop:disable CodeReuse/ActiveRecord
        runners_to_be_deleted =
          Ci::Runner
            .where(id: authorized_runners_ids)
            .preload([:taggings, :runner_namespaces, :runner_projects])
        # rubocop:enable CodeReuse/ActiveRecord
        deleted_ids = runners_to_be_deleted.destroy_all.map(&:id) # rubocop:disable Cop/DestroyAll

        ServiceResponse.success(
          payload: {
            deleted_count: deleted_ids.count,
            deleted_ids: deleted_ids,
            errors: error_messages(runner_count, authorized_runners_ids, unauthorized_runners_ids)
          })
      end

      def compute_authorized_runners
        # rubocop:disable CodeReuse/ActiveRecord
        @current_user.ci_owned_runners.load # preload the owned runners to avoid an N+1
        authorized_runners, unauthorized_runners =
          @runners.limit(RUNNER_LIMIT)
                  .partition { |runner| Ability.allowed?(@current_user, :delete_runner, runner) }
        # rubocop:enable CodeReuse/ActiveRecord

        [authorized_runners.map(&:id), unauthorized_runners.map(&:id)]
      end

      def error_messages(runner_count, authorized_runners_ids, unauthorized_runners_ids)
        errors = []

        if runner_count > RUNNER_LIMIT
          errors << "Can only delete up to #{RUNNER_LIMIT} runners per call. Ignored the remaining runner(s)."
        end

        if authorized_runners_ids.empty?
          errors << "User does not have permission to delete any of the runners"
        elsif unauthorized_runners_ids.any?
          failed_ids = unauthorized_runners_ids.map { |runner_id| "##{runner_id}" }.join(', ')
          errors << "User does not have permission to delete runner(s) #{failed_ids}"
        end

        errors
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(**service_args).execute }

  let_it_be(:admin_user) { create(:user, :admin) }
  let_it_be_with_refind(:owner_user) { create(:user) } # discard memoized ci_owned_runners
  let_it_be(:group) { create(:group) }
  let_it_be(:project) { create(:project, group: group) }

  let(:user) {}
  let(:service_args) { { runners: runners_arg, current_user: user } }
  let(:runners_arg) {}

  context 'with runners specified' do
    let!(:instance_runner) { create(:ci_runner) }
    let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
    let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }

    shared_examples 'a service deleting runners in bulk' do
      let!(:expected_deleted_ids) { expected_deleted_runners.map(&:id) }

      it 'destroys runners', :aggregate_failures do
        expect { execute }.to change { Ci::Runner.count }.by(-expected_deleted_ids.count)

        is_expected.to be_success
        expect(execute.payload).to eq(
          {
            deleted_count: expected_deleted_ids.count,
            deleted_ids: expected_deleted_ids,
            errors: []
          })
        expect { project_runner.runner_projects.first.reload }.to raise_error(ActiveRecord::RecordNotFound)
        expected_deleted_runners.each do |deleted_runner|
          expect(deleted_runner[:errors]).to be_nil
          expect { deleted_runner.reload }.to raise_error(ActiveRecord::RecordNotFound)
        end
      end

      context 'with too many runners specified' do
        before do
          stub_const("#{described_class}::RUNNER_LIMIT", 1)
        end

        it 'deletes only first RUNNER_LIMIT runners', :aggregate_failures do
          expect { execute }.to change { Ci::Runner.count }.by(-1)

          is_expected.to be_success
          expect(execute.payload).to eq(
            {
              deleted_count: 1,
              deleted_ids: expected_deleted_ids.take(1),
              errors: ["Can only delete up to 1 runners per call. Ignored the remaining runner(s)."]
            })
        end
      end
    end

    context 'when the user cannot delete runners' do
      let(:runners_arg) { Ci::Runner.all }

      context 'when user is not group owner' do
        before do
          group.add_developer(user)
        end

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

        it 'does not delete any runner and returns error', :aggregate_failures do
          expect { execute }.not_to change { Ci::Runner.count }
          expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
        end
      end

      context 'when user is not part of the group' do
        let(:user) { create(:user) }

        it 'does not delete any runner and returns error', :aggregate_failures do
          expect { execute }.not_to change { Ci::Runner.count }
          expect(execute[:errors]).to match_array("User does not have permission to delete any of the runners")
        end
      end
    end

    context 'when the user can delete runners' do
      context 'when user is an admin', :enable_admin_mode do
        include_examples 'a service deleting runners in bulk' do
          let(:runners_arg) { Ci::Runner.all }
          let!(:expected_deleted_runners) { [instance_runner, group_runner, project_runner] }
          let(:user) { admin_user }
        end

        context 'with a runner already deleted' do
          before do
            group_runner.destroy!
          end

          include_examples 'a service deleting runners in bulk' do
            let(:runners_arg) { Ci::Runner.all }
            let!(:expected_deleted_runners) { [instance_runner, project_runner] }
            let(:user) { admin_user }
          end
        end

        context 'when deleting a single runner' do
          let(:runners_arg) { Ci::Runner.all }

          it 'avoids N+1 cached queries', :use_sql_query_cache, :request_store do
            # Run this once to establish a baseline
            control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
              execute
            end

            additional_runners = 1

            create_list(:ci_runner, 1 + additional_runners, :instance)
            create_list(:ci_runner, 1 + additional_runners, :group, groups: [group])
            create_list(:ci_runner, 1 + additional_runners, :project, projects: [project])

            service = described_class.new(runners: runners_arg, current_user: user)

            # Base cost per runner is:
            #  - 1 `SELECT * FROM "taggings"` query
            #  - 1 `SAVEPOINT` query
            #  - 1 `DELETE FROM "ci_runners"` query
            #  - 1 `RELEASE SAVEPOINT` query
            # Project runners have an additional query:
            #  - 1 `DELETE FROM "ci_runner_projects"` query, given the call to `destroy_all`
            instance_runner_cost = 4
            group_runner_cost = 4
            project_runner_cost = 5
            expect { service.execute }
              .not_to exceed_all_query_limit(control_count)
              .with_threshold(additional_runners * (instance_runner_cost + group_runner_cost + project_runner_cost))
          end
        end
      end

      context 'when user is group owner' do
        before do
          group.add_owner(user)
        end

        include_examples 'a service deleting runners in bulk' do
          let(:runners_arg) { Ci::Runner.not_instance_type }
          let!(:expected_deleted_runners) { [group_runner, project_runner] }
          let(:user) { owner_user }
        end

        context 'with a runner non-authorised to be deleted' do
          let(:runners_arg) { Ci::Runner.all }
          let!(:expected_deleted_runners) { [project_runner] }
          let(:user) { owner_user }

          it 'destroys only authorised runners', :aggregate_failures do
            allow(Ability).to receive(:allowed?).and_call_original
            expect(Ability).to receive(:allowed?).with(user, :delete_runner, instance_runner).and_return(false)

            expect { execute }.to change { Ci::Runner.count }.by(-2)

            is_expected.to be_success
            expect(execute.payload).to eq(
              {
                deleted_count: 2,
                deleted_ids: [group_runner.id, project_runner.id],
                errors: ["User does not have permission to delete runner(s) ##{instance_runner.id}"]
              })
          end
        end
      end
    end

    context 'with no arguments specified' do
      let(:runners_arg) { nil }
      let(:user) { owner_user }

      it 'returns 0 deleted runners' do
        is_expected.to be_success
        expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [], errors: [] })
      end
    end
  end
end


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

module Ci
  module Runners
    class UnregisterRunnerManagerService
      attr_reader :runner, :author, :system_id

      # @param [Ci::Runner] runner the runner to unregister/destroy
      # @param [User, authentication token String] author the user or the authentication token authorizing the removal
      # @param [String] system_id ID of the system being unregistered
      def initialize(runner, author, system_id:)
        @runner = runner
        @author = author
        @system_id = system_id
      end

      def execute
        return system_id_missing_error if system_id.blank?

        runner_manager = runner.runner_managers.find_by_system_xid!(system_id)
        runner_manager.destroy!

        ServiceResponse.success
      end

      private

      def system_id_missing_error
        ServiceResponse.error(message: '`system_id` needs to be specified for runners created in the UI.')
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::UnregisterRunnerManagerService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(runner, 'some_token', system_id: system_id).execute }

  context 'with runner registered with registration token' do
    let!(:runner) { create(:ci_runner, registration_type: :registration_token) }
    let(:system_id) { nil }

    it 'does not destroy runner or runner managers' do
      expect do
        expect(execute).to be_error
      end.to not_change { Ci::Runner.count }
         .and not_change { Ci::RunnerManager.count }
      expect(runner[:errors]).to be_nil
    end
  end

  context 'with runner created in UI' do
    let!(:runner_manager1) { create(:ci_runner_machine, runner: runner, system_xid: 'system_id_1') }
    let!(:runner_manager2) { create(:ci_runner_machine, runner: runner, system_xid: 'system_id_2') }
    let!(:runner) { create(:ci_runner, registration_type: :authenticated_user) }

    context 'with system_id specified' do
      let(:system_id) { runner_manager1.system_xid }

      it 'destroys runner_manager1 and leaves runner', :aggregate_failures do
        expect do
          expect(execute).to be_success
        end.to change { Ci::RunnerManager.count }.by(-1)
           .and not_change { Ci::Runner.count }
        expect(runner[:errors]).to be_nil
        expect(runner.runner_managers).to contain_exactly(runner_manager2)
      end
    end

    context 'with unknown system_id' do
      let(:system_id) { 'unknown_system_id' }

      it 'raises RecordNotFound error', :aggregate_failures do
        expect do
          execute
        end.to raise_error(ActiveRecord::RecordNotFound)
           .and not_change { Ci::Runner.count }
           .and not_change { Ci::RunnerManager.count }
      end
    end

    context 'with system_id missing' do
      let(:system_id) { nil }

      it 'returns error and leaves runner_manager1', :aggregate_failures do
        expect do
          expect(execute).to be_error
          expect(execute.message).to eq('`system_id` needs to be specified for runners created in the UI.')
        end.to not_change { Ci::Runner.count }
           .and not_change { Ci::RunnerManager.count }
      end
    end
  end
end


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

module Ci
  module Runners
    class UnassignRunnerService
      # @param [Ci::RunnerProject] runner_project the runner/project association to destroy
      # @param [User] user the user performing the operation
      def initialize(runner_project, user)
        @runner_project = runner_project
        @runner = runner_project.runner
        @project = runner_project.project
        @user = user
      end

      def execute
        unless @user.present? && @user.can?(:assign_runner, @runner)
          return ServiceResponse.error(message: 'user not allowed to assign runner')
        end

        if @runner_project.destroy
          ServiceResponse.success
        else
          ServiceResponse.error(message: 'failed to destroy runner project')
        end
      end

      private

      attr_reader :runner, :project, :user
    end
  end
end

Ci::Runners::UnassignRunnerService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute', feature_category: :runner_fleet do
  let_it_be(:project) { create(:project) }
  let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) }

  let(:runner_project) { runner.runner_projects.last }

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

  context 'without user' do
    let(:user) { nil }

    it 'does not destroy runner_project', :aggregate_failures do
      expect(runner_project).not_to receive(:destroy)
      expect { execute }.not_to change { runner.runner_projects.count }.from(1)

      is_expected.to be_error
    end
  end

  context 'with unauthorized user' do
    let(:user) { build(:user) }

    it 'does not call destroy on runner_project' do
      expect(runner).not_to receive(:destroy)

      is_expected.to be_error
    end
  end

  context 'with admin user', :enable_admin_mode do
    let(:user) { create_default(:user, :admin) }

    context 'with destroy returning false' do
      it 'returns error response' do
        expect(runner_project).to receive(:destroy).once.and_return(false)

        is_expected.to be_error
      end
    end

    context 'with destroy returning true' do
      it 'returns success response' do
        expect(runner_project).to receive(:destroy).once.and_return(true)

        is_expected.to be_success
      end
    end
  end
end


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

module Ci
  module Runners
    class CreateRunnerService
      RUNNER_CLASS_MAPPING = {
        'instance_type' => Ci::Runners::RunnerCreationStrategies::InstanceRunnerStrategy,
        'group_type' => Ci::Runners::RunnerCreationStrategies::GroupRunnerStrategy,
        'project_type' => Ci::Runners::RunnerCreationStrategies::ProjectRunnerStrategy
      }.freeze

      def initialize(user:, params:)
        @user = user
        @params = params
        @strategy = RUNNER_CLASS_MAPPING[params[:runner_type]].new(user: user, params: params)
      end

      def execute
        normalize_params

        error = strategy.validate_params
        return ServiceResponse.error(message: error, reason: :validation_error) if error

        unless strategy.authorized_user?
          return ServiceResponse.error(message: _('Insufficient permissions'), reason: :forbidden)
        end

        runner = ::Ci::Runner.new(params)

        return ServiceResponse.success(payload: { runner: runner }) if runner.save

        ServiceResponse.error(message: runner.errors.full_messages, reason: :save_error)
      end

      def normalize_params
        params[:registration_type] = :authenticated_user
        params[:active] = !params.delete(:paused) if params.key?(:paused)
        params[:creator] = user

        strategy.normalize_params
      end

      private

      attr_reader :user, :params, :strategy
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do
  subject(:execute) { described_class.new(user: current_user, params: params).execute }

  let(:runner) { execute.payload[:runner] }

  let_it_be(:admin) { create(:admin) }
  let_it_be(:non_admin_user) { create(:user) }
  let_it_be(:anonymous) { nil }
  let_it_be(:group_owner) { create(:user) }

  let_it_be(:group) { create(:group) }

  shared_examples 'it can create a runner' do
    it 'creates a runner of the specified type', :aggregate_failures do
      is_expected.to be_success
      expect(runner.runner_type).to eq expected_type
    end

    context 'with default params provided' do
      let(:args) do
        {}
      end

      before do
        params.merge!(args)
      end

      it { is_expected.to be_success }

      it 'uses default values when none are provided' do
        expect(runner).to be_an_instance_of(::Ci::Runner)
        expect(runner.persisted?).to be_truthy
        expect(runner.run_untagged).to be true
        expect(runner.active).to be true
        expect(runner.creator).to be current_user
        expect(runner.authenticated_user_registration_type?).to be_truthy
        expect(runner.runner_type).to eq expected_type
      end
    end

    context 'with non-default params provided' do
      let(:args) do
        {
          description: 'some description',
          maintenance_note: 'a note',
          paused: true,
          tag_list: %w[tag1 tag2],
          access_level: 'ref_protected',
          locked: true,
          maximum_timeout: 600,
          run_untagged: false
        }
      end

      before do
        params.merge!(args)
      end

      it { is_expected.to be_success }

      it 'creates runner with specified values', :aggregate_failures do
        expect(runner).to be_an_instance_of(::Ci::Runner)
        expect(runner.description).to eq 'some description'
        expect(runner.maintenance_note).to eq 'a note'
        expect(runner.active).to eq !args[:paused]
        expect(runner.locked).to eq args[:locked]
        expect(runner.run_untagged).to eq args[:run_untagged]
        expect(runner.tags).to contain_exactly(
          an_object_having_attributes(name: 'tag1'),
          an_object_having_attributes(name: 'tag2')
        )
        expect(runner.access_level).to eq args[:access_level]
        expect(runner.maximum_timeout).to eq args[:maximum_timeout]

        expect(runner.authenticated_user_registration_type?).to be_truthy
        expect(runner.runner_type).to eq expected_type
      end

      context 'with a nil paused value' do
        let(:args) do
          {
            paused: nil,
            description: 'some description',
            maintenance_note: 'a note',
            tag_list: %w[tag1 tag2],
            access_level: 'ref_protected',
            locked: true,
            maximum_timeout: 600,
            run_untagged: false
          }
        end

        it { is_expected.to be_success }

        it 'creates runner with active set to true' do
          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.active).to eq true
        end
      end

      context 'with no paused value given' do
        let(:args) do
          {
            description: 'some description',
            maintenance_note: 'a note',
            tag_list: %w[tag1 tag2],
            access_level: 'ref_protected',
            locked: true,
            maximum_timeout: 600,
            run_untagged: false
          }
        end

        it { is_expected.to be_success }

        it 'creates runner with active set to true' do
          expect(runner).to be_an_instance_of(::Ci::Runner)
          expect(runner.active).to eq true
        end
      end
    end
  end

  shared_examples 'it cannot create a runner' do
    it 'runner payload is nil' do
      expect(runner).to be nil
    end

    it { is_expected.to be_error }
  end

  shared_examples 'it can return an error' do
    let(:runner_double) { Ci::Runner.new }

    context 'when the runner fails to save' do
      before do
        allow(Ci::Runner).to receive(:new).and_return runner_double
      end

      it_behaves_like 'it cannot create a runner'

      it 'returns error message' do
        expect(execute.errors).not_to be_empty
      end
    end
  end

  context 'with :runner_type param set to instance_type' do
    let(:expected_type) { 'instance_type' }
    let(:params) { { runner_type: 'instance_type' } }

    context 'when anonymous user' do
      let(:current_user) { anonymous }

      it_behaves_like 'it cannot create a runner'
    end

    context 'when non-admin user' do
      let(:current_user) { non_admin_user }

      it_behaves_like 'it cannot create a runner'
    end

    context 'when admin user' do
      let(:current_user) { admin }

      it_behaves_like 'it cannot create a runner'

      context 'when admin mode is enabled', :enable_admin_mode do
        it_behaves_like 'it can create a runner'
        it_behaves_like 'it can return an error'

        context 'with unexpected scope param specified' do
          let(:params) { { runner_type: 'instance_type', scope: group } }

          it_behaves_like 'it cannot create a runner'
        end

        context 'when model validation fails' do
          let(:params) { { runner_type: 'instance_type', run_untagged: false, tag_list: [] } }

          it_behaves_like 'it cannot create a runner'

          it 'returns error message and reason', :aggregate_failures do
            expect(execute.reason).to eq(:save_error)
            expect(execute.message).to contain_exactly(a_string_including('Tags list can not be empty'))
          end
        end
      end
    end
  end

  context 'with :runner_type param set to group_type' do
    let(:expected_type) { 'group_type' }
    let(:params) { { runner_type: 'group_type', scope: group } }

    before do
      group.add_developer(non_admin_user)
      group.add_owner(group_owner)
    end

    context 'when anonymous user' do
      let(:current_user) { anonymous }

      it_behaves_like 'it cannot create a runner'
    end

    context 'when non-admin user' do
      let(:current_user) { non_admin_user }

      it_behaves_like 'it cannot create a runner'
    end

    context 'when group owner' do
      let(:current_user) { group_owner }

      it_behaves_like 'it can create a runner'

      context 'with missing scope param' do
        let(:params) { { runner_type: 'group_type' } }

        it_behaves_like 'it cannot create a runner'
      end
    end

    context 'when admin user' do
      let(:current_user) { admin }

      it_behaves_like 'it cannot create a runner'

      context 'when admin mode is enabled', :enable_admin_mode do
        it_behaves_like 'it can create a runner'
        it_behaves_like 'it can return an error'
      end
    end
  end

  context 'with :runner_type param set to project_type' do
    let_it_be(:project) { create(:project, namespace: group) }

    let(:expected_type) { 'project_type' }
    let(:params) { { runner_type: 'project_type', scope: project } }

    before do
      group.add_developer(non_admin_user)
      group.add_owner(group_owner)
    end

    context 'when anonymous user' do
      let(:current_user) { anonymous }

      it_behaves_like 'it cannot create a runner'
    end

    context 'when group owner' do
      let(:current_user) { group_owner }

      it_behaves_like 'it can create a runner'

      context 'with missing scope param' do
        let(:params) { { runner_type: 'project_type' } }

        it_behaves_like 'it cannot create a runner'
      end
    end

    context 'when non-admin user' do
      let(:current_user) { non_admin_user }

      it_behaves_like 'it cannot create a runner'

      context 'with project permissions to create runner' do
        before do
          project.add_maintainer(current_user)
        end

        it_behaves_like 'it can create a runner'
      end
    end

    context 'when admin user' do
      let(:current_user) { admin }

      it_behaves_like 'it cannot create a runner'

      context 'when admin mode is enabled', :enable_admin_mode do
        it_behaves_like 'it can create a runner'
        it_behaves_like 'it can return an error'
      end
    end
  end
end


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

module Ci
  module Runners
    class AssignRunnerService
      # @param [Ci::Runner] runner: the runner to assign to a project
      # @param [Project] project: the new project to assign the runner to
      # @param [User] user: the user performing the operation
      def initialize(runner, project, user)
        @runner = runner
        @project = project
        @user = user
      end

      def execute
        unless @user.present? && @user.can?(:assign_runner, @runner)
          return ServiceResponse.error(message: 'user not allowed to assign runner', http_status: :forbidden)
        end

        unless @user.can?(:register_project_runners, @project)
          return ServiceResponse.error(message: 'user not allowed to add runners to project', http_status: :forbidden)
        end

        if @runner.assign_to(@project, @user)
          ServiceResponse.success
        else
          ServiceResponse.error(message: 'failed to assign runner')
        end
      end

      private

      attr_reader :runner, :project, :user
    end
  end
end

Ci::Runners::AssignRunnerService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(runner, new_project, user).execute }

  let_it_be(:owner_group) { create(:group) }
  let_it_be(:owner_project) { create(:project, group: owner_group) }
  let_it_be(:new_project) { create(:project) }
  let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project]) }

  context 'without user' do
    let(:user) { nil }

    it 'does not call assign_to on runner and returns error response', :aggregate_failures do
      expect(runner).not_to receive(:assign_to)

      is_expected.to be_error
      expect(execute.message).to eq('user not allowed to assign runner')
    end
  end

  context 'with unauthorized user' do
    let(:user) { build(:user) }

    it 'does not call assign_to on runner and returns error message' do
      expect(runner).not_to receive(:assign_to)

      is_expected.to be_error
      expect(execute.message).to eq('user not allowed to assign runner')
    end
  end

  context 'with authorized user' do
    let(:user) { create(:user) }

    context 'with user owning runner and being maintainer of new project' do
      before do
        owner_project.group.add_owner(user)
        new_project.add_maintainer(user)
      end

      it 'calls assign_to on runner and returns success response' do
        expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original

        is_expected.to be_success
      end
    end

    context 'with user owning runner' do
      before do
        owner_project.add_maintainer(user)
      end

      it 'does not call assign_to on runner and returns error message', :aggregate_failures do
        expect(runner).not_to receive(:assign_to)

        is_expected.to be_error
        expect(execute.message).to eq('user not allowed to add runners to project')
      end
    end

    context 'with user being maintainer of new project', :aggregate_failures do
      before do
        new_project.add_maintainer(user)
      end

      it 'does not call assign_to on runner and returns error message' do
        expect(runner).not_to receive(:assign_to)

        is_expected.to be_error
        expect(execute.message).to eq('user not allowed to assign runner')
      end
    end
  end

  context 'with admin user', :enable_admin_mode do
    let(:user) { create(:user, :admin) }

    it 'calls assign_to on runner and returns success response' do
      expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original

      is_expected.to be_success
    end
  end
end


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

module Ci
  module Runners
    class ReconcileExistingRunnerVersionsService
      VERSION_BATCH_SIZE = 100

      def execute
        insert_result = insert_runner_versions
        total_deleted = cleanup_runner_versions(insert_result[:versions_from_runners])
        total_updated = update_status_on_outdated_runner_versions(insert_result[:versions_from_runners])

        ServiceResponse.success(payload: {
          total_inserted: insert_result[:new_record_count],
          total_updated: total_updated,
          total_deleted: total_deleted
        })
      end

      private

      def upgrade_check
        @runner_upgrade_check ||= Gitlab::Ci::RunnerUpgradeCheck.new(::Gitlab::VERSION)
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def insert_runner_versions
        versions_from_runners = Set[]
        new_record_count = 0
        Ci::Runner.distinct_each_batch(column: :version, of: VERSION_BATCH_SIZE) do |version_batch|
          batch_versions = version_batch.pluck(:version).to_set
          versions_from_runners += batch_versions

          # Avoid hitting primary DB
          already_existing_versions = Ci::RunnerVersion.where(version: batch_versions).pluck(:version)
          new_versions = batch_versions - already_existing_versions

          if new_versions.any?
            new_record_count += Ci::RunnerVersion.insert_all(
              new_versions.map { |v| { version: v } },
              returning: :version,
              unique_by: :version).count
          end
        end

        { versions_from_runners: versions_from_runners, new_record_count: new_record_count }
      end

      def cleanup_runner_versions(versions_from_runners)
        Ci::RunnerVersion.where.not(version: versions_from_runners).delete_all
      end
      # rubocop: enable CodeReuse/ActiveRecord

      def outdated_runner_versions
        Ci::RunnerVersion.potentially_outdated
      end

      def update_status_on_outdated_runner_versions(versions_from_runners)
        total_updated = 0

        outdated_runner_versions.each_batch(of: VERSION_BATCH_SIZE) do |version_batch|
          updated = version_batch
            .select { |runner_version| versions_from_runners.include?(runner_version['version']) }
            .filter_map { |runner_version| runner_version_with_updated_status(runner_version) }

          if updated.any?
            total_updated += Ci::RunnerVersion.upsert_all(updated, unique_by: :version).count
          end
        end

        total_updated
      end

      def runner_version_with_updated_status(runner_version)
        _, new_status = upgrade_check.check_runner_upgrade_suggestion(runner_version.version)

        if new_status != :error && new_status != runner_version.status.to_sym
          {
            version: runner_version.version,
            status: Ci::RunnerVersion.statuses[new_status]
          }
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute', feature_category: :runner_fleet do
  include RunnerReleasesHelper

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

  let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') }
  let_it_be(:runner_version_14_0_1) do
    create(:ci_runner_version, version: '14.0.1', status: :unavailable)
  end

  context 'with RunnerUpgradeCheck recommending 14.0.2' do
    let(:upgrade_check) { instance_double(::Gitlab::Ci::RunnerUpgradeCheck) }

    before do
      stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1)

      allow(::Gitlab::Ci::RunnerUpgradeCheck).to receive(:new).and_return(upgrade_check).once
    end

    context 'with runner with new version' do
      let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') }
      let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
      let!(:runner_version_14_0_0) do
        create(:ci_runner_version, version: '14.0.0', status: :unavailable)
      end

      before do
        allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
          .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended])
        allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
          .with('14.0.2')
          .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
          .once
      end

      it 'creates and updates expected ci_runner_versions entries', :aggregate_failures do
        expect(Ci::RunnerVersion).to receive(:insert_all)
          .ordered
          .with([{ version: '14.0.2' }], anything)
          .once
          .and_call_original

        expect { execute }
          .to change { runner_version_14_0_0.reload.status }.from('unavailable').to('recommended')
          .and change { runner_version_14_0_1.reload.status }.from('unavailable').to('recommended')
          .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('unavailable')

        expect(execute).to be_success
        expect(execute.payload).to eq({
          total_inserted: 1, # 14.0.2 is inserted
          total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated
          total_deleted: 0
        })
      end
    end

    context 'with orphan ci_runner_version' do
      let!(:runner_version_14_0_2) do
        create(:ci_runner_version, version: '14.0.2', status: :unavailable)
      end

      before do
        allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
          .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable])
      end

      it 'deletes orphan ci_runner_versions entry', :aggregate_failures do
        expect { execute }
          .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('unavailable').to(nil)
          .and not_change { runner_version_14_0_1.reload.status }.from('unavailable')

        expect(execute).to be_success
        expect(execute.payload).to eq({
          total_inserted: 0,
          total_updated: 0,
          total_deleted: 1 # 14.0.2 is deleted
        })
      end
    end

    context 'with no runner version changes' do
      before do
        allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
          .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :unavailable])
      end

      it 'does not modify ci_runner_versions entries', :aggregate_failures do
        expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')

        expect(execute).to be_success
        expect(execute.payload).to eq({
          total_inserted: 0,
          total_updated: 0,
          total_deleted: 0
        })
      end
    end

    context 'with failing version check' do
      before do
        allow(upgrade_check).to receive(:check_runner_upgrade_suggestion)
          .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :error])
      end

      it 'makes no changes to ci_runner_versions', :aggregate_failures do
        expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')

        expect(execute).to be_success
        expect(execute.payload).to eq({
          total_inserted: 0,
          total_updated: 0,
          total_deleted: 0
        })
      end
    end
  end

  context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do
    before do
      stub_runner_releases(%w[14.0.0 14.0.1])
    end

    it 'does not modify ci_runner_versions entries', :aggregate_failures do
      expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('unavailable')

      expect(execute).to be_success
      expect(execute.payload).to eq({
        total_inserted: 0,
        total_updated: 0,
        total_deleted: 0
      })
    end
  end
end


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

module Ci
  module Runners
    class ResetRegistrationTokenService
      # @param [ApplicationSetting, Project, Group] scope: the scope of the reset operation
      # @param [User] user: the user performing the operation
      def initialize(scope, user)
        @scope = scope
        @user = user
      end

      def execute
        unless @user.present? && @user.can?(:update_runners_registration_token, scope)
          return ServiceResponse.error(message: 'user not allowed to update runners registration token')
        end

        if scope.respond_to?(:runners_registration_token)
          scope.reset_runners_registration_token!
          runners_token = scope.runners_registration_token
        else
          scope.reset_runners_token!
          runners_token = scope.runners_token
        end

        ServiceResponse.success(payload: { new_registration_token: runners_token })
      end

      private

      attr_reader :scope, :user
    end
  end
end

Ci::Runners::ResetRegistrationTokenService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute', feature_category: :runner_fleet do
  subject(:execute) { described_class.new(scope, current_user).execute }

  let_it_be(:user) { build(:user) }
  let_it_be(:admin_user) { create(:user, :admin) }

  shared_examples 'a registration token reset operation' do
    context 'without user' do
      let(:current_user) { nil }

      it 'does not reset registration token and returns error response' do
        expect(scope).not_to receive(token_reset_method_name)

        expect(execute).to be_error
      end
    end

    context 'with unauthorized user' do
      let(:current_user) { user }

      it 'does not reset registration token and returns error response' do
        expect(scope).not_to receive(token_reset_method_name)

        expect(execute).to be_error
      end
    end

    context 'with admin user', :enable_admin_mode do
      let(:current_user) { admin_user }

      it 'resets registration token and returns value unchanged' do
        expect(scope).to receive(token_reset_method_name).once do
          expect(scope).to receive(token_method_name).once.and_return("#{token_method_name} return value")
        end

        expect(execute).to be_success
        expect(execute.payload[:new_registration_token]).to eq("#{token_method_name} return value")
      end
    end
  end

  context 'with instance scope' do
    let_it_be(:scope) { create(:application_setting) }

    before do
      allow(ApplicationSetting).to receive(:current).and_return(scope)
      allow(ApplicationSetting).to receive(:current_without_cache).and_return(scope)
    end

    it_behaves_like 'a registration token reset operation' do
      let(:token_method_name) { :runners_registration_token }
      let(:token_reset_method_name) { :reset_runners_registration_token! }
    end
  end

  context 'with group scope' do
    let_it_be(:scope) { create(:group) }

    it_behaves_like 'a registration token reset operation' do
      let(:token_method_name) { :runners_token }
      let(:token_reset_method_name) { :reset_runners_token! }
    end
  end

  context 'with project scope' do
    let_it_be(:scope) { create(:project) }

    it_behaves_like 'a registration token reset operation' do
      let(:token_method_name) { :runners_token }
      let(:token_reset_method_name) { :reset_runners_token! }
    end
  end
end