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

module Ci
  module JobArtifacts
    class CreateService < ::BaseService
      include Gitlab::Utils::UsageData

      LSIF_ARTIFACT_TYPE = 'lsif'

      OBJECT_STORAGE_ERRORS = [
        Errno::EIO,
        Google::Apis::ServerError,
        Signet::RemoteServerError
      ].freeze

      def initialize(job)
        @job = job
        @project = job.project
        @pipeline = job.pipeline
      end

      def authorize(artifact_type:, filesize: nil)
        result = validate_requirements(artifact_type: artifact_type, filesize: filesize)
        return result unless result[:status] == :success

        headers = JobArtifactUploader.workhorse_authorize(
          has_length: false,
          maximum_size: max_size(artifact_type),
          use_final_store_path: true,
          final_store_path_root_id: project.id
        )

        if lsif?(artifact_type)
          headers[:ProcessLsif] = true
          track_usage_event('i_source_code_code_intelligence', project.id)
        end

        success(headers: headers)
      end

      def execute(artifacts_file, params, metadata_file: nil)
        result = validate_requirements(artifact_type: params[:artifact_type], filesize: artifacts_file.size)
        return result unless result[:status] == :success

        return success if sha256_matches_existing_artifact?(params[:artifact_type], artifacts_file)

        build_result = build_artifact(artifacts_file, params, metadata_file)
        return build_result unless build_result[:status] == :success

        artifact = build_result[:artifact]
        artifact_metadata = build_result[:artifact_metadata]

        track_artifact_uploader(artifact)

        parse_result = parse_artifact(artifact)
        return parse_result unless parse_result[:status] == :success

        persist_artifact(artifact, artifact_metadata)
      end

      private

      attr_reader :job, :project, :pipeline

      def validate_requirements(artifact_type:, filesize:)
        return too_large_error if too_large?(artifact_type, filesize)

        success
      end

      def too_large?(type, size)
        size > max_size(type) if size
      end

      def lsif?(type)
        type == LSIF_ARTIFACT_TYPE
      end

      def max_size(type)
        Ci::JobArtifact.max_artifact_size(type: type, project: project)
      end

      def too_large_error
        error('file size has reached maximum size limit', :payload_too_large)
      end

      def build_artifact(artifacts_file, params, metadata_file)
        artifact_attributes = {
          job: job,
          project: project,
          expire_in: expire_in(params),
          accessibility: accessibility(params),
          locked: pipeline.locked
        }

        file_attributes = {
          file_type: params[:artifact_type],
          file_format: params[:artifact_format],
          file_sha256: artifacts_file.sha256,
          file: artifacts_file
        }

        artifact = Ci::JobArtifact.new(artifact_attributes.merge(file_attributes))

        artifact_metadata = build_metadata_artifact(artifact, metadata_file) if metadata_file

        success(artifact: artifact, artifact_metadata: artifact_metadata)
      end

      def build_metadata_artifact(job_artifact, metadata_file)
        Ci::JobArtifact.new(
          job: job_artifact.job,
          project: job_artifact.project,
          expire_at: job_artifact.expire_at,
          locked: job_artifact.locked,
          file: metadata_file,
          file_type: :metadata,
          file_format: :gzip,
          file_sha256: metadata_file.sha256,
          accessibility: job_artifact.accessibility
        )
      end

      def expire_in(params)
        params['expire_in'] || Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
      end

      def accessibility(params)
        accessibility = params[:accessibility]

        return :public if Feature.disabled?(:non_public_artifacts, project, type: :development)

        return accessibility if accessibility.present?

        job.artifact_is_public_in_config? ? :public : :private
      end

      def parse_artifact(artifact)
        case artifact.file_type
        when 'dotenv' then parse_dotenv_artifact(artifact)
        when 'annotations' then parse_annotations_artifact(artifact)
        else success
        end
      end

      def persist_artifact(artifact, artifact_metadata)
        job.transaction do
          # NOTE: The `artifacts_expire_at` column is already deprecated and to be removed in the near future.
          # Running it first because in migrations we lock the `ci_builds` table
          # first and then the others. This reduces the chances of deadlocks.
          job.update_column(:artifacts_expire_at, artifact.expire_at)

          artifact.save!
          artifact_metadata&.save!
        end

        success(artifact: artifact)
      rescue ActiveRecord::RecordNotUnique => error
        track_exception(error, artifact.file_type)
        error('another artifact of the same type already exists', :bad_request)
      rescue *OBJECT_STORAGE_ERRORS => error
        track_exception(error, artifact.file_type)
        error(error.message, :service_unavailable)
      rescue StandardError => error
        track_exception(error, artifact.file_type)
        error(error.message, :bad_request)
      end

      def sha256_matches_existing_artifact?(artifact_type, artifacts_file)
        existing_artifact = job.job_artifacts.find_by_file_type(artifact_type)
        return false unless existing_artifact

        existing_artifact.file_sha256 == artifacts_file.sha256
      end

      def track_exception(error, artifact_type)
        Gitlab::ErrorTracking.track_exception(
          error,
          job_id: job.id,
          project_id: job.project_id,
          uploading_type: artifact_type
        )
      end

      def track_artifact_uploader(_artifact)
        # Overridden in EE
      end

      def parse_dotenv_artifact(artifact)
        Ci::ParseDotenvArtifactService.new(project, current_user).execute(artifact)
      end

      def parse_annotations_artifact(artifact)
        Ci::ParseAnnotationsArtifactService.new(project, current_user).execute(artifact)
      end
    end
  end
end

Ci::JobArtifacts::CreateService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::CreateService, :clean_gitlab_redis_shared_state, feature_category: :build_artifacts do
  include WorkhorseHelpers
  include Gitlab::Utils::Gzip

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

  let(:service) { described_class.new(job) }
  let(:job) { create(:ci_build, project: project) }

  describe '#authorize', :aggregate_failures do
    let(:artifact_type) { 'archive' }
    let(:filesize) { nil }

    subject(:authorize) { service.authorize(artifact_type: artifact_type, filesize: filesize) }

    shared_examples_for 'handling lsif artifact' do
      context 'when artifact is lsif' do
        let(:artifact_type) { 'lsif' }

        it 'includes ProcessLsif in the headers' do
          expect(authorize[:headers][:ProcessLsif]).to eq(true)
        end
      end
    end

    shared_examples_for 'validating requirements' do
      context 'when filesize is specified' do
        let(:max_artifact_size) { 10 }

        before do
          allow(Ci::JobArtifact)
            .to receive(:max_artifact_size)
            .with(type: artifact_type, project: project)
            .and_return(max_artifact_size)
        end

        context 'and filesize exceeds the limit' do
          let(:filesize) { max_artifact_size + 1 }

          it 'returns error' do
            expect(authorize[:status]).to eq(:error)
          end
        end

        context 'and filesize does not exceed the limit' do
          let(:filesize) { max_artifact_size - 1 }

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

    shared_examples_for 'uploading to temp location' do |store_type|
      # We are not testing the entire headers here because this is fully tested
      # in workhorse_authorize's spec. We just want to confirm that it indeed used the temp path
      # by checking some indicators in the headers returned.
      if store_type == :object_storage
        it 'includes the authorize headers' do
          expect(authorize[:status]).to eq(:success)
          expect(authorize[:headers][:RemoteObject][:StoreURL]).to include(ObjectStorage::TMP_UPLOAD_PATH)
        end
      else
        it 'includes the authorize headers' do
          expect(authorize[:status]).to eq(:success)
          expect(authorize[:headers][:TempPath]).to include(ObjectStorage::TMP_UPLOAD_PATH)
        end
      end

      it_behaves_like 'handling lsif artifact'
      it_behaves_like 'validating requirements'
    end

    context 'when object storage is enabled' do
      context 'and direct upload is enabled' do
        let(:final_store_path) { '12/34/abc-123' }

        before do
          stub_artifacts_object_storage(JobArtifactUploader, direct_upload: true)

          allow(JobArtifactUploader)
            .to receive(:generate_final_store_path)
            .with(root_id: project.id)
            .and_return(final_store_path)
        end

        it 'includes the authorize headers' do
          expect(authorize[:status]).to eq(:success)

          expect(authorize[:headers][:RemoteObject][:ID]).to eq(final_store_path)

          # We are not testing the entire headers here because this is fully tested
          # in workhorse_authorize's spec. We just want to confirm that it indeed used the final path
          # by checking some indicators in the headers returned.
          expect(authorize[:headers][:RemoteObject][:StoreURL])
            .to include(final_store_path)

          # We have to ensure to tell Workhorse to skip deleting the file after upload
          # because we are uploading the file to its final location
          expect(authorize[:headers][:RemoteObject][:SkipDelete]).to eq(true)
        end

        it_behaves_like 'handling lsif artifact'
        it_behaves_like 'validating requirements'
      end

      context 'and direct upload is disabled' do
        before do
          stub_artifacts_object_storage(JobArtifactUploader, direct_upload: false)
        end

        it_behaves_like 'uploading to temp location', :local_storage
      end
    end

    context 'when object storage is disabled' do
      it_behaves_like 'uploading to temp location', :local_storage
    end
  end

  describe '#execute' do
    let(:artifacts_sha256) { '0' * 64 }
    let(:metadata_file) { nil }

    let(:params) do
      {
        'artifact_type' => 'archive',
        'artifact_format' => 'zip'
      }.with_indifferent_access
    end

    subject(:execute) { service.execute(artifacts_file, params, metadata_file: metadata_file) }

    shared_examples_for 'handling accessibility' do
      shared_examples 'public accessibility' do
        it 'sets accessibility to public level' do
          subject

          expect(job.job_artifacts).not_to be_empty
          expect(job.job_artifacts).to all be_public_accessibility
        end
      end

      shared_examples 'private accessibility' do
        it 'sets accessibility to private level' do
          subject

          expect(job.job_artifacts).not_to be_empty
          expect(job.job_artifacts).to all be_private_accessibility
        end
      end

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

        it_behaves_like 'public accessibility'
      end

      context 'when non_public_artifacts flag is enabled' do
        context 'and accessibility is defined in the params' do
          context 'and is passed as private' do
            before do
              params.merge!('accessibility' => 'private')
            end

            it_behaves_like 'private accessibility'
          end

          context 'and is passed as public' do
            before do
              params.merge!('accessibility' => 'public')
            end

            it_behaves_like 'public accessibility'
          end
        end

        context 'and accessibility is not defined in the params' do
          context 'and job has no public artifacts defined in its CI config' do
            it_behaves_like 'public accessibility'
          end

          context 'and job artifacts defined as private in the CI config' do
            let(:job) { create(:ci_build, :with_private_artifacts_config, project: project) }

            it_behaves_like 'private accessibility'
          end

          context 'and job artifacts defined as public in the CI config' do
            let(:job) { create(:ci_build, :with_public_artifacts_config, project: project) }

            it_behaves_like 'public accessibility'
          end
        end
      end

      context 'when accessibility passed as invalid value' do
        before do
          params.merge!('accessibility' => 'foo')
        end

        it 'fails with argument error' do
          expect { execute }.to raise_error(ArgumentError, "'foo' is not a valid accessibility")
        end
      end
    end

    shared_examples_for 'handling metadata file' do
      context 'when metadata file is also uploaded' do
        let(:metadata_file) do
          file_to_upload('spec/fixtures/ci_build_artifacts_metadata.gz', sha256: artifacts_sha256)
        end

        before do
          stub_application_setting(default_artifacts_expire_in: '1 day')
        end

        it 'creates a new metadata job artifact' do
          expect { execute }.to change { Ci::JobArtifact.where(file_type: :metadata).count }.by(1)

          new_artifact = job.job_artifacts.last
          expect(new_artifact.project).to eq(job.project)
          expect(new_artifact.file).to be_present
          expect(new_artifact.file_type).to eq('metadata')
          expect(new_artifact.file_format).to eq('gzip')
          expect(new_artifact.file_sha256).to eq(artifacts_sha256)
          expect(new_artifact.locked).to eq(job.pipeline.locked)
        end

        it 'logs the created artifact and metadata' do
          expect(Gitlab::Ci::Artifacts::Logger)
            .to receive(:log_created)
            .with(an_instance_of(Ci::JobArtifact)).twice

          subject
        end

        it_behaves_like 'handling accessibility'

        it 'sets expiration date according to application settings' do
          expected_expire_at = 1.day.from_now

          expect(execute).to match(a_hash_including(status: :success, artifact: anything))
          archive_artifact, metadata_artifact = job.job_artifacts.last(2)

          expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
          expect(archive_artifact.expire_at).to be_within(1.minute).of(expected_expire_at)
          expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at)
        end

        context 'when expire_in params is set to a specific value' do
          before do
            params.merge!('expire_in' => '2 hours')
          end

          it 'sets expiration date according to the parameter' do
            expected_expire_at = 2.hours.from_now

            expect(execute).to match(a_hash_including(status: :success, artifact: anything))
            archive_artifact, metadata_artifact = job.job_artifacts.last(2)

            expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
            expect(archive_artifact.expire_at).to be_within(1.minute).of(expected_expire_at)
            expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at)
          end
        end

        context 'when expire_in params is set to `never`' do
          before do
            params.merge!('expire_in' => 'never')
          end

          it 'sets expiration date according to the parameter' do
            expected_expire_at = nil

            expect(execute).to be_truthy
            archive_artifact, metadata_artifact = job.job_artifacts.last(2)

            expect(job.artifacts_expire_at).to eq(expected_expire_at)
            expect(archive_artifact.expire_at).to eq(expected_expire_at)
            expect(metadata_artifact.expire_at).to eq(expected_expire_at)
          end
        end
      end
    end

    shared_examples_for 'handling dotenv' do |storage_type|
      context 'when artifact type is dotenv' do
        let(:params) do
          {
            'artifact_type' => 'dotenv',
            'artifact_format' => 'gzip'
          }.with_indifferent_access
        end

        if storage_type == :object_storage
          let(:object_body) { File.read('spec/fixtures/build.env.gz') }
          let(:upload_filename) { 'build.env.gz' }

          before do
            stub_request(:get, %r{s3.amazonaws.com/#{remote_path}})
              .to_return(status: 200, body: File.read('spec/fixtures/build.env.gz'))
          end
        else
          let(:artifacts_file) do
            file_to_upload('spec/fixtures/build.env.gz', sha256: artifacts_sha256)
          end
        end

        it 'calls parse service' do
          expect_any_instance_of(Ci::ParseDotenvArtifactService) do |service|
            expect(service).to receive(:execute).once.and_call_original
          end

          expect(execute[:status]).to eq(:success)
          expect(job.job_variables.as_json(only: [:key, :value, :source])).to contain_exactly(
            hash_including('key' => 'KEY1', 'value' => 'VAR1', 'source' => 'dotenv'),
            hash_including('key' => 'KEY2', 'value' => 'VAR2', 'source' => 'dotenv'))
        end
      end
    end

    shared_examples_for 'handling annotations' do |storage_type|
      context 'when artifact type is annotations' do
        let(:params) do
          {
            'artifact_type' => 'annotations',
            'artifact_format' => 'gzip'
          }.with_indifferent_access
        end

        if storage_type == :object_storage
          let(:object_body) { File.read('spec/fixtures/gl-annotations.json.gz') }
          let(:upload_filename) { 'gl-annotations.json.gz' }

          before do
            stub_request(:get, %r{s3.amazonaws.com/#{remote_path}})
              .to_return(status: 200, body: File.read('spec/fixtures/gl-annotations.json.gz'))
          end
        else
          let(:artifacts_file) do
            file_to_upload('spec/fixtures/gl-annotations.json.gz', sha256: artifacts_sha256)
          end
        end

        it 'calls parse service' do
          expect_any_instance_of(Ci::ParseAnnotationsArtifactService) do |service|
            expect(service).to receive(:execute).once.and_call_original
          end

          expect(execute[:status]).to eq(:success)
          expect(job.job_annotations.as_json).to contain_exactly(
            hash_including('name' => 'external_links', 'data' => [
              hash_including('external_link' => hash_including('label' => 'URL 1', 'url' => 'https://url1.example.com/')),
              hash_including('external_link' => hash_including('label' => 'URL 2', 'url' => 'https://url2.example.com/'))
            ])
          )
        end
      end
    end

    shared_examples_for 'handling object storage errors' do
      shared_examples 'rescues object storage error' do |klass, message, expected_message|
        it "handles #{klass}" do
          allow_next_instance_of(JobArtifactUploader) do |uploader|
            allow(uploader).to receive(:store!).and_raise(klass, message)
          end

          expect(Gitlab::ErrorTracking)
            .to receive(:track_exception)
            .and_call_original

          expect(execute).to match(
            a_hash_including(
              http_status: :service_unavailable,
              message: expected_message || message,
              status: :error))
        end
      end

      it_behaves_like 'rescues object storage error',
        Errno::EIO, 'some/path', 'Input/output error - some/path'

      it_behaves_like 'rescues object storage error',
        Google::Apis::ServerError, 'Server error'

      it_behaves_like 'rescues object storage error',
        Signet::RemoteServerError, 'The service is currently unavailable'
    end

    shared_examples_for 'validating requirements' do
      context 'when filesize is specified' do
        let(:max_artifact_size) { 10 }

        before do
          allow(Ci::JobArtifact)
            .to receive(:max_artifact_size)
            .with(type: 'archive', project: project)
            .and_return(max_artifact_size)

          allow(artifacts_file).to receive(:size).and_return(filesize)
        end

        context 'and filesize exceeds the limit' do
          let(:filesize) { max_artifact_size + 1 }

          it 'returns error' do
            expect(execute[:status]).to eq(:error)
          end
        end

        context 'and filesize does not exceed the limit' do
          let(:filesize) { max_artifact_size - 1 }

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

    shared_examples_for 'handling existing artifact' do
      context 'when job already has an artifact of the same file type' do
        let!(:existing_artifact) do
          create(:ci_job_artifact, params[:artifact_type], file_sha256: existing_sha256, job: job)
        end

        context 'when sha256 of uploading artifact is the same of the existing one' do
          let(:existing_sha256) { artifacts_sha256 }

          it 'ignores the changes' do
            expect { execute }.not_to change { Ci::JobArtifact.count }
            expect(execute).to match(a_hash_including(status: :success))
          end
        end

        context 'when sha256 of uploading artifact is different than the existing one' do
          let(:existing_sha256) { '1' * 64 }

          it 'returns error status' do
            expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original

            expect { execute }.not_to change { Ci::JobArtifact.count }
            expect(execute).to match(
              a_hash_including(
                http_status: :bad_request,
                message: 'another artifact of the same type already exists',
                status: :error
              )
            )
          end
        end
      end
    end

    shared_examples_for 'logging artifact' do
      it 'logs the created artifact' do
        expect(Gitlab::Ci::Artifacts::Logger)
          .to receive(:log_created)
          .with(an_instance_of(Ci::JobArtifact))

        execute
      end
    end

    shared_examples_for 'handling uploads' do
      context 'when artifacts file is uploaded' do
        it 'creates a new job artifact' do
          expect { execute }.to change { Ci::JobArtifact.count }.by(1)

          new_artifact = execute[:artifact]
          expect(new_artifact).to eq(job.job_artifacts.last)
          expect(new_artifact.project).to eq(job.project)
          expect(new_artifact.file.filename).to eq(artifacts_file.original_filename)
          expect(new_artifact.file_identifier).to eq(artifacts_file.original_filename)
          expect(new_artifact.file_type).to eq(params['artifact_type'])
          expect(new_artifact.file_format).to eq(params['artifact_format'])
          expect(new_artifact.file_sha256).to eq(artifacts_sha256)
          expect(new_artifact.locked).to eq(job.pipeline.locked)
          expect(new_artifact.size).to eq(artifacts_file.size)

          expect(execute[:status]).to eq(:success)
        end

        it_behaves_like 'handling accessibility'
        it_behaves_like 'handling metadata file'
        it_behaves_like 'handling partitioning'
        it_behaves_like 'logging artifact'
      end
    end

    shared_examples_for 'handling partitioning' do
      context 'with job partitioned', :ci_partitionable do
        let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
        let(:job) { create(:ci_build, pipeline: pipeline) }

        it 'sets partition_id on artifacts' do
          expect { execute }.to change { Ci::JobArtifact.count }

          artifacts_partitions = job.job_artifacts.map(&:partition_id).uniq

          expect(artifacts_partitions).to eq([ci_testing_partition_id])
        end
      end
    end

    context 'when object storage and direct upload is enabled' do
      let(:fog_connection) { stub_artifacts_object_storage(JobArtifactUploader, direct_upload: true) }
      let(:remote_path) { File.join(remote_store_path, remote_id) }
      let(:object_body) { File.open('spec/fixtures/ci_build_artifacts.zip') }
      let(:upload_filename) { 'artifacts.zip' }
      let(:object) do
        fog_connection.directories
          .new(key: 'artifacts')
          .files
          .create( # rubocop:disable Rails/SaveBang
            key: remote_path,
            body: object_body
          )
      end

      let(:artifacts_file) do
        fog_to_uploaded_file(
          object,
          filename: upload_filename,
          sha256: artifacts_sha256,
          remote_id: remote_id
        )
      end

      let(:remote_id) { 'generated-remote-id-12345' }
      let(:remote_store_path) { ObjectStorage::TMP_UPLOAD_PATH }

      it_behaves_like 'handling uploads'
      it_behaves_like 'handling dotenv', :object_storage
      it_behaves_like 'handling annotations', :object_storage
      it_behaves_like 'handling object storage errors'
      it_behaves_like 'validating requirements'
    end

    context 'when using local storage' do
      let(:artifacts_file) do
        file_to_upload('spec/fixtures/ci_build_artifacts.zip', sha256: artifacts_sha256)
      end

      it_behaves_like 'handling uploads'
      it_behaves_like 'handling dotenv', :local_storage
      it_behaves_like 'handling annotations', :local_storage
      it_behaves_like 'validating requirements'
    end
  end

  def file_to_upload(path, params = {})
    upload = Tempfile.new('upload')
    FileUtils.copy(path, upload.path)
    # This is a workaround for https://github.com/docker/for-linux/issues/1015
    FileUtils.touch(upload.path)

    UploadedFile.new(upload.path, **params)
  end
end


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

module Ci
  module JobArtifacts
    class DeleteService
      include BaseServiceUtility

      def initialize(build)
        @build = build
      end

      def execute
        if build.project.refreshing_build_artifacts_size?
          Gitlab::ProjectStatsRefreshConflictsLogger.warn_artifact_deletion_during_stats_refresh(
            method: 'Ci::JobArtifacts::DeleteService#execute',
            project_id: build.project_id
          )
          return ServiceResponse.error(
            message: 'Action temporarily disabled. The project this job belongs to is undergoing stats refresh.',
            reason: :project_stats_refresh
          )
        end

        result = Ci::JobArtifacts::DestroyBatchService.new(build.job_artifacts.erasable).execute

        if result.fetch(:status) == :success
          ServiceResponse.success(payload:
          {
            destroyed_artifacts_count: result.fetch(:destroyed_artifacts_count)
          })
        else
          ServiceResponse.error(message: result.fetch(:message))
        end
      end

      private

      attr_reader :build
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::DeleteService, feature_category: :build_artifacts do
  let_it_be(:build, reload: true) do
    create(:ci_build, :artifacts, :trace_artifact, artifacts_expire_at: 100.days.from_now)
  end

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

  describe '#execute' do
    it 'is successful' do
      result = service.execute

      expect(result).to be_success
      expect(result[:destroyed_artifacts_count]).to be(2)
    end

    it 'deletes erasable artifacts' do
      expect { service.execute }.to change { build.job_artifacts.erasable.count }.from(2).to(0)
    end

    it 'does not delete trace' do
      expect { service.execute }.not_to change { build.has_trace? }.from(true)
    end

    context 'when project is undergoing stats refresh' do
      before do
        allow(build.project).to receive(:refreshing_build_artifacts_size?).and_return(true)
      end

      it 'logs a warning' do
        expect(Gitlab::ProjectStatsRefreshConflictsLogger)
          .to receive(:warn_artifact_deletion_during_stats_refresh)
                .with(method: 'Ci::JobArtifacts::DeleteService#execute', project_id: build.project_id)

        service.execute
      end

      it 'returns an error response with the correct message and reason' do
        result = service.execute

        expect(result).to be_error
        expect(result[:message]).to be('Action temporarily disabled. ' \
          'The project this job belongs to is undergoing stats refresh.')
        expect(result[:reason]).to be(:project_stats_refresh)
      end
    end

    context 'when an error response is received from DestroyBatchService' do
      before do
        allow_next_instance_of(Ci::JobArtifacts::DestroyBatchService) do |service|
          allow(service).to receive(:execute).and_return({ status: :error, message: 'something went wrong' })
        end
      end

      it 'returns an error response with the correct message' do
        result = service.execute

        expect(result).to be_error
        expect(result[:message]).to be('something went wrong')
      end
    end
  end
end


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

module Ci
  module JobArtifacts
    class ExpireProjectBuildArtifactsService
      BATCH_SIZE = 1000

      def initialize(project_id, expiry_time)
        @project_id = project_id
        @expiry_time = expiry_time
      end

      # rubocop:disable CodeReuse/ActiveRecord
      def execute
        scope = Ci::JobArtifact.for_project(project_id).order(:id)
        file_type_values = Ci::JobArtifact.erasable_file_types.map { |file_type| [Ci::JobArtifact.file_types[file_type]] }
        from_sql = Arel::Nodes::Grouping.new(Arel::Nodes::ValuesList.new(file_type_values)).as('file_types (file_type)').to_sql
        array_scope = Ci::JobArtifact.from(from_sql).select(:file_type)
        array_mapping_scope = -> (file_type_expression) { Ci::JobArtifact.where(Ci::JobArtifact.arel_table[:file_type].eq(file_type_expression)) }

        Gitlab::Pagination::Keyset::Iterator
          .new(scope: scope, in_operator_optimization_options: { array_scope: array_scope, array_mapping_scope: array_mapping_scope })
          .each_batch(of: BATCH_SIZE) do |batch|
          ids = batch.reselect!(:id).to_a.map(&:id)
          Ci::JobArtifact.unlocked.where(id: ids).update_all(locked: Ci::JobArtifact.lockeds[:unlocked], expire_at: expiry_time)
        end
      end
      # rubocop:enable CodeReuse/ActiveRecord

      private

      attr_reader :project_id, :expiry_time
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::JobArtifacts::ExpireProjectBuildArtifactsService, feature_category: :build_artifacts do
  let_it_be(:project) { create(:project) }
  let_it_be(:pipeline, reload: true) { create(:ci_pipeline, :unlocked, project: project) }

  let(:expiry_time) { Time.current }

  RSpec::Matchers.define :have_locked_status do |expected_status|
    match do |job_artifacts|
      predicate = "#{expected_status}?".to_sym
      job_artifacts.all? { |artifact| artifact.__send__(predicate) }
    end
  end

  RSpec::Matchers.define :expire_at do |expected_expiry|
    match do |job_artifacts|
      job_artifacts.all? { |artifact| artifact.expire_at.to_i == expected_expiry.to_i }
    end
  end

  RSpec::Matchers.define :have_no_expiry do
    match do |job_artifacts|
      job_artifacts.all? { |artifact| artifact.expire_at.nil? }
    end
  end

  describe '#execute' do
    subject(:execute) { described_class.new(project.id, expiry_time).execute }

    context 'with job containing erasable artifacts' do
      let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }

      it 'unlocks erasable job artifacts' do
        execute

        expect(job.job_artifacts).to have_locked_status(:artifact_unlocked)
      end

      it 'expires erasable job artifacts' do
        execute

        expect(job.job_artifacts).to expire_at(expiry_time)
      end
    end

    context 'with job containing trace artifacts' do
      let_it_be(:job, reload: true) { create(:ci_build, :trace_artifact, pipeline: pipeline) }

      it 'does not unlock trace artifacts' do
        execute

        expect(job.job_artifacts).to have_locked_status(:artifact_unknown)
      end

      it 'does not expire trace artifacts' do
        execute

        expect(job.job_artifacts).to have_no_expiry
      end
    end

    context 'with job from artifact locked pipeline' do
      let_it_be(:job, reload: true) { create(:ci_build, pipeline: pipeline) }
      let_it_be(:locked_artifact, reload: true) { create(:ci_job_artifact, :locked, job: job) }

      before do
        pipeline.artifacts_locked!
      end

      it 'does not unlock locked artifacts' do
        execute

        expect(job.job_artifacts).to have_locked_status(:artifact_artifacts_locked)
      end

      it 'does not expire locked artifacts' do
        execute

        expect(job.job_artifacts).to have_no_expiry
      end
    end

    context 'with job containing both erasable and trace artifacts' do
      let_it_be(:job, reload: true) { create(:ci_build, pipeline: pipeline) }
      let_it_be(:erasable_artifact, reload: true) { create(:ci_job_artifact, :archive, job: job) }
      let_it_be(:trace_artifact, reload: true) { create(:ci_job_artifact, :trace, job: job) }

      it 'unlocks erasable artifacts' do
        execute

        expect(erasable_artifact.artifact_unlocked?).to be_truthy
      end

      it 'expires erasable artifacts' do
        execute

        expect(erasable_artifact.expire_at.to_i).to eq(expiry_time.to_i)
      end

      it 'does not unlock trace artifacts' do
        execute

        expect(trace_artifact.artifact_unlocked?).to be_falsey
      end

      it 'does not expire trace artifacts' do
        execute

        expect(trace_artifact.expire_at).to be_nil
      end
    end

    context 'with multiple pipelines' do
      let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }

      let_it_be(:pipeline2, reload: true) { create(:ci_pipeline, :unlocked, project: project) }
      let_it_be(:job2, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }

      it 'unlocks artifacts across pipelines' do
        execute

        expect(job.job_artifacts).to have_locked_status(:artifact_unlocked)
        expect(job2.job_artifacts).to have_locked_status(:artifact_unlocked)
      end

      it 'expires artifacts across pipelines' do
        execute

        expect(job.job_artifacts).to expire_at(expiry_time)
        expect(job2.job_artifacts).to expire_at(expiry_time)
      end
    end

    context 'with artifacts belonging to another project' do
      let_it_be(:job, reload: true) { create(:ci_build, :erasable, pipeline: pipeline) }

      let_it_be(:another_project, reload: true) { create(:project) }
      let_it_be(:another_pipeline, reload: true) { create(:ci_pipeline, project: another_project) }
      let_it_be(:another_job, reload: true) { create(:ci_build, :erasable, pipeline: another_pipeline) }

      it 'does not unlock erasable artifacts in other projects' do
        execute

        expect(another_job.job_artifacts).to have_locked_status(:artifact_unknown)
      end

      it 'does not expire erasable artifacts in other projects' do
        execute

        expect(another_job.job_artifacts).to have_no_expiry
      end
    end
  end
end


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

module Ci
  module PipelineSchedules
    class CalculateNextRunService < BaseService
      include Gitlab::Utils::StrongMemoize

      def execute(schedule, fallback_method:)
        @schedule = schedule

        return fallback_method.call unless plan_cron&.cron_valid?

        now = Time.zone.now
        plan_min_run = plan_cron.next_time_from(now)

        schedule_next_run = schedule_cron.next_time_from(now)
        return schedule_next_run if worker_cron.match?(schedule_next_run) && plan_min_run <= schedule_next_run

        plan_next_run = plan_cron.next_time_from(schedule_next_run)
        return plan_next_run if worker_cron.match?(plan_next_run)

        worker_next_run = worker_cron.next_time_from(schedule_next_run)
        return worker_next_run if plan_min_run <= worker_next_run

        worker_cron.next_time_from(plan_next_run)
      end

      private

      def schedule_cron
        strong_memoize(:schedule_cron) do
          Gitlab::Ci::CronParser.new(@schedule.cron, @schedule.cron_timezone)
        end
      end

      def worker_cron
        strong_memoize(:worker_cron) do
          Gitlab::Ci::CronParser.new(@schedule.worker_cron_expression, Time.zone.name)
        end
      end

      def plan_cron
        strong_memoize(:plan_cron) do
          daily_limit = @schedule.daily_limit

          next unless daily_limit

          every_x_minutes = (1.day.in_minutes / daily_limit).to_i

          Gitlab::Ci::CronParser.parse_natural("every #{every_x_minutes} minutes", Time.zone.name)
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true
# rubocop:disable Layout/LineLength
require 'spec_helper'

RSpec.describe Ci::PipelineSchedules::CalculateNextRunService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project, :public, :repository) }

  describe '#execute' do
    using RSpec::Parameterized::TableSyntax

    let(:run_service) do
      described_class.new(project).execute(pipeline_schedule,
        fallback_method: pipeline_schedule.method(:calculate_next_run_at))
    end

    let(:pipeline_schedule) { create(:ci_pipeline_schedule, cron: schedule_cron) }
    let(:daily_limit_of_144_runs) { 1.day / 10.minutes }
    let(:daily_limit_of_24_runs) { 1.day / 1.hour }

    before do
      allow(Settings).to receive(:cron_jobs) { { 'pipeline_schedule_worker' => { 'cron' => worker_cron } } }
      create(:plan_limits, :default_plan, ci_daily_pipeline_schedule_triggers: plan_limit) if plan_limit
    end

    context "when there is invalid or no plan limits" do
      where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
        '0 1 2 3 *'   | '0 1 * * *'   | nil             | Time.zone.local(2021, 3, 2, 1, 0)   | Time.zone.local(2022, 3, 2, 1, 0)
        '*/5 * * * *' | '*/1 * * * *' | nil             | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 5)
        '*/5 * * * *' | '0 * * * *'   | nil             | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
        # 1.day / 2.hours => 12 times a day and it is invalid because there is a minimum for plan limits.
        # See: https://docs.gitlab.com/ee/administration/instance_limits.html#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day
        '*/5 * * * *' | '0 * * * *'   | 1.day / 2.hours | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 5)
      end

      with_them do
        it 'calls fallback method to get next_run_at' do
          travel_to(now) do
            expect(pipeline_schedule).to receive(:calculate_next_run_at).and_call_original

            result = run_service

            expect(result).to eq(expected_result)
          end
        end
      end
    end

    context "when the workers next run matches schedule's earliest run" do
      where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
        '*/5 * * * *' | '0 * * * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 11, 0)  | Time.zone.local(2021, 5, 27, 12, 0)
        '*/5 * * * *' | '*/5 * * * *' | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 11, 0)  | Time.zone.local(2021, 5, 27, 11, 10)
        '*/5 * * * *' | '0 1 * * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 1, 0)   | Time.zone.local(2021, 5, 28, 1, 0)
        '*/5 * * * *' | '0 2 * * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 1, 0)   | Time.zone.local(2021, 5, 27, 2, 0)
        '*/5 * * * *' | '0 3 * * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 1, 0)   | Time.zone.local(2021, 5, 27, 3, 0)
        '*/5 * * * *' | '0 1 1 * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 1, 1, 0)    | Time.zone.local(2021, 6, 1, 1, 0)
        '*/9 * * * *' | '0 1 1 * *'   | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 1, 1, 9)    | Time.zone.local(2021, 6, 1, 1, 0)
        '*/5 * * * *' | '45 21 1 2 *' | daily_limit_of_144_runs  | Time.zone.local(2021, 2, 1, 21, 45)  | Time.zone.local(2022, 2, 1, 21, 45)
      end

      with_them do
        it 'calculates the next_run_at to be earliest point of match' do
          travel_to(now) do
            result = run_service

            expect(result).to eq(expected_result)
          end
        end
      end
    end

    context "when next_run_at is restricted by plan limit" do
      where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
        '*/5 * * * *' | '59 14 * * *' | daily_limit_of_24_runs   | Time.zone.local(2021, 5, 1, 15, 0)  | Time.zone.local(2021, 5, 2, 15, 0)
        '*/5 * * * *' | '*/1 * * * *' | daily_limit_of_24_runs   | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 12, 0)
        '*/5 * * * *' | '*/1 * * * *' | daily_limit_of_144_runs  | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
        '*/5 * * * *' | '*/1 * * * *' | (1.day / 7.minutes).to_i | Time.zone.local(2021, 5, 27, 11, 0) | Time.zone.local(2021, 5, 27, 11, 10)
      end

      with_them do
        it 'calculates the next_run_at based on next available limit' do
          travel_to(now) do
            result = run_service

            expect(result).to eq(expected_result)
          end
        end
      end
    end

    context "when next_run_at is restricted by worker's availability" do
      where(:worker_cron, :schedule_cron, :plan_limit, :now, :expected_result) do
        '0 1 2 3 *' | '0 1 * * *' | daily_limit_of_144_runs | Time.zone.local(2021, 3, 2, 1, 0) | Time.zone.local(2022, 3, 2, 1, 0)
      end

      with_them do
        it 'calculates the next_run_at using worker_cron' do
          travel_to(now) do
            result = run_service

            expect(result).to eq(expected_result)
          end
        end
      end
    end
  end
end
# rubocop:enable Layout/LineLength


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

module Ci
  module PipelineSchedules
    class TakeOwnershipService
      def initialize(schedule, user)
        @schedule = schedule
        @user = user
      end

      def execute
        return forbidden unless allowed?

        if schedule.update(owner: user)
          ServiceResponse.success(payload: schedule)
        else
          ServiceResponse.error(message: schedule.errors.full_messages)
        end
      end

      private

      attr_reader :schedule, :user

      def allowed?
        user.can?(:admin_pipeline_schedule, schedule)
      end

      def forbidden
        ServiceResponse.error(message: _('Failed to change the owner'), reason: :access_denied)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineSchedules::TakeOwnershipService, feature_category: :continuous_integration do
  let_it_be(:user) { create(:user) }
  let_it_be(:owner) { create(:user) }
  let_it_be(:reporter) { create(:user) }
  let_it_be(:project) { create(:project, :public, :repository) }
  let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: owner) }

  before_all do
    project.add_maintainer(user)
    project.add_maintainer(owner)
    project.add_reporter(reporter)
  end

  describe '#execute' do
    context 'when user does not have permission' do
      subject(:service) { described_class.new(pipeline_schedule, reporter) }

      it 'returns ServiceResponse.error' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.error?).to be(true)
        expect(result.message).to eq(_('Failed to change the owner'))
      end
    end

    context 'when user has permission' do
      subject(:service) { described_class.new(pipeline_schedule, user) }

      it 'returns ServiceResponse.success' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.success?).to be(true)
        expect(result.payload).to eq(pipeline_schedule)
      end

      context 'when schedule update fails' do
        subject(:service) { described_class.new(pipeline_schedule, owner) }

        before do
          allow(pipeline_schedule).to receive(:update).and_return(false)

          errors = ActiveModel::Errors.new(pipeline_schedule)
          errors.add(:base, 'An error occurred')
          allow(pipeline_schedule).to receive(:errors).and_return(errors)
        end

        it 'returns ServiceResponse.error' do
          result = service.execute

          expect(result).to be_a(ServiceResponse)
          expect(result.error?).to be(true)
          expect(result.message).to eq(['An error occurred'])
        end
      end
    end
  end
end


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

module Ci
  module PipelineSchedules
    class UpdateService < BaseSaveService
      AUTHORIZE = :update_pipeline_schedule

      def initialize(schedule, user, params)
        @schedule = schedule
        @user = user
        @project = schedule.project
        @params = params
      end

      def execute
        return forbidden_to_save unless allowed_to_save?

        super
      end

      private

      def authorize_message
        _('The current user is not authorized to update the pipeline schedule')
      end
      strong_memoize_attr :authorize_message
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuous_integration do
  let_it_be_with_reload(:user) { create(:user) }
  let_it_be_with_reload(:project) { create(:project, :public, :repository) }
  let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
  let_it_be(:reporter) { create(:user) }
  let_it_be(:project_owner) { create(:user) }

  let_it_be(:pipeline_schedule_variable) do
    create(:ci_pipeline_schedule_variable,
      key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
  end

  before_all do
    project.add_maintainer(user)
    project.add_owner(project_owner)
    project.add_reporter(reporter)

    pipeline_schedule.reload
  end

  describe "execute" do
    context 'when user does not have permission' do
      subject(:service) { described_class.new(pipeline_schedule, reporter, {}) }

      it 'returns ServiceResponse.error' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.error?).to be(true)

        error_message = _('The current user is not authorized to update the pipeline schedule')
        expect(result.message).to match_array([error_message])
        expect(pipeline_schedule.errors).to match_array([error_message])
      end
    end

    context 'when user has permission' do
      let(:params) do
        {
          description: 'updated_desc',
          ref: 'patch-x',
          active: false,
          cron: '*/1 * * * *',
          variables_attributes: [
            { id: pipeline_schedule_variable.id, key: 'bar', secret_value: 'barvalue' }
          ]
        }
      end

      subject(:service) { described_class.new(pipeline_schedule, user, params) }

      it 'updates database values with passed params' do
        expect do
          service.execute
          pipeline_schedule.reload
        end.to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc')
          .and change { pipeline_schedule.ref }.from('master').to('patch-x')
          .and change { pipeline_schedule.active }.from(true).to(false)
          .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *')
          .and change { pipeline_schedule.variables.last.key }.from('foo').to('bar')
          .and change { pipeline_schedule.variables.last.value }.from('foovalue').to('barvalue')
      end

      context 'when the new branch is protected', :request_store do
        let(:maintainer_access) { :no_one_can_merge }

        before do
          create(:protected_branch, :no_one_can_push, maintainer_access, name: 'patch-x', project: project)
        end

        after do
          ProtectedBranches::CacheService.new(project).refresh
        end

        context 'when called by someone other than the schedule owner who can update the ref' do
          let(:maintainer_access) { :maintainers_can_merge }

          subject(:service) { described_class.new(pipeline_schedule, project_owner, params) }

          it 'does not update the schedule' do
            expect do
              service.execute
              pipeline_schedule.reload
            end.not_to change { pipeline_schedule.description }
          end
        end

        context 'when called by the schedule owner' do
          it 'does not update the schedule' do
            expect do
              service.execute
              pipeline_schedule.reload
            end.not_to change { pipeline_schedule.description }
          end

          context 'when the owner can update the ref' do
            let(:maintainer_access) { :maintainers_can_merge }

            it 'updates the schedule' do
              expect { service.execute }.to change { pipeline_schedule.description }
            end
          end
        end
      end

      context 'when creating a variable' do
        let(:params) do
          {
            variables_attributes: [
              { key: 'ABC', secret_value: 'ABC123' }
            ]
          }
        end

        it 'creates the new variable' do
          expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(1)

          expect(pipeline_schedule.variables.last.key).to eq('ABC')
          expect(pipeline_schedule.variables.last.value).to eq('ABC123')
        end
      end

      context 'when deleting a variable' do
        let(:params) do
          {
            variables_attributes: [
              {
                id: pipeline_schedule_variable.id,
                _destroy: true
              }
            ]
          }
        end

        it 'deletes the existing variable' do
          expect { service.execute }.to change { Ci::PipelineScheduleVariable.count }.by(-1)
        end
      end

      it 'returns ServiceResponse.success' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.success?).to be(true)
        expect(result.payload.description).to eq('updated_desc')
      end

      context 'when schedule update fails' do
        subject(:service) { described_class.new(pipeline_schedule, user, {}) }

        before do
          allow(pipeline_schedule).to receive(:save).and_return(false)

          errors = ActiveModel::Errors.new(pipeline_schedule)
          errors.add(:base, 'An error occurred')
          allow(pipeline_schedule).to receive(:errors).and_return(errors)
        end

        it 'returns ServiceResponse.error' do
          result = service.execute

          expect(result).to be_a(ServiceResponse)
          expect(result.error?).to be(true)
          expect(result.message).to match_array(['An error occurred'])
        end
      end
    end

    it_behaves_like 'pipeline schedules checking variables permission' do
      subject(:service) { described_class.new(pipeline_schedule, user, params) }
    end
  end
end


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

module Ci
  module PipelineSchedules
    class CreateService < BaseSaveService
      AUTHORIZE = :create_pipeline_schedule

      def initialize(project, user, params)
        @schedule = project.pipeline_schedules.new
        @user = user
        @project = project
        @params = params.merge(owner: user)
      end

      private

      def authorize_message
        _('The current user is not authorized to create the pipeline schedule')
      end
      strong_memoize_attr :authorize_message
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuous_integration do
  let_it_be(:reporter) { create(:user) }
  let_it_be_with_reload(:user) { create(:user) }
  let_it_be_with_reload(:project) { create(:project, :public, :repository) }

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

  before_all do
    project.add_maintainer(user)
    project.add_reporter(reporter)
  end

  describe "execute" do
    context 'when user does not have permission' do
      subject(:service) { described_class.new(project, reporter, {}) }

      it 'returns ServiceResponse.error' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.error?).to be(true)

        error_message = _('The current user is not authorized to create the pipeline schedule')
        expect(result.message).to match_array([error_message])
        expect(result.payload.errors).to match_array([error_message])
      end
    end

    context 'when user has permission' do
      let(:params) do
        {
          description: 'desc',
          ref: 'patch-x',
          active: false,
          cron: '*/1 * * * *',
          cron_timezone: 'UTC'
        }
      end

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

      it 'saves values with passed params' do
        result = service.execute

        expect(result.payload).to have_attributes(
          description: 'desc',
          ref: 'patch-x',
          active: false,
          cron: '*/1 * * * *',
          cron_timezone: 'UTC'
        )
      end

      it 'returns ServiceResponse.success' do
        result = service.execute

        expect(result).to be_a(ServiceResponse)
        expect(result.success?).to be(true)
      end

      context 'when schedule save fails' do
        subject(:service) { described_class.new(project, user, {}) }

        before do
          errors = ActiveModel::Errors.new(project)
          errors.add(:base, 'An error occurred')

          allow_next_instance_of(Ci::PipelineSchedule) do |instance|
            allow(instance).to receive(:save).and_return(false)
            allow(instance).to receive(:errors).and_return(errors)
          end
        end

        it 'returns ServiceResponse.error' do
          result = service.execute

          expect(result).to be_a(ServiceResponse)
          expect(result.error?).to be(true)
          expect(result.message).to match_array(['An error occurred'])
        end
      end
    end

    it_behaves_like 'pipeline schedules checking variables permission'
  end
end


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

module Ci
  module PipelineCreation
    class CancelRedundantPipelinesService
      include Gitlab::Utils::StrongMemoize

      BATCH_SIZE = 25
      PAGE_SIZE = 500

      def initialize(pipeline)
        @pipeline = pipeline
        @project = @pipeline.project
      end

      # rubocop: disable CodeReuse/ActiveRecord
      def execute
        return if service_disabled?
        return if pipeline.parent_pipeline? # skip if child pipeline
        return unless project.auto_cancel_pending_pipelines?

        paginator.each do |ids|
          pipelines = parent_and_child_pipelines(ids)

          Gitlab::OptimisticLocking.retry_lock(pipelines, name: 'cancel_pending_pipelines') do |cancelables|
            auto_cancel_interruptible_pipelines(cancelables.ids)
          end
        end
      end

      private

      attr_reader :pipeline, :project

      def paginator
        page = 1
        Enumerator.new do |yielder|
          loop do
            # leverage the index_ci_pipelines_on_project_id_and_status_and_created_at index
            records = project.all_pipelines
              .created_after(pipelines_created_after)
              .order(:status, :created_at)
              .page(page) # use offset pagination because there is no other way to loop over the data
              .per(PAGE_SIZE)
              .pluck(:id)

            raise StopIteration if records.empty?

            yielder << records
            page += 1
          end
        end
      end

      def parent_auto_cancelable_pipelines(ids)
        scope = project.all_pipelines
          .created_after(pipelines_created_after)
          .for_ref(pipeline.ref)
          .where_not_sha(project.commit(pipeline.ref).try(:id))
          .where("created_at < ?", pipeline.created_at)
          .for_status(CommitStatus::AVAILABLE_STATUSES) # Force usage of project_id_and_status_and_created_at_index
          .ci_sources

        scope.id_in(ids)
      end

      def parent_and_child_pipelines(ids)
        Ci::Pipeline.object_hierarchy(parent_auto_cancelable_pipelines(ids), project_condition: :same)
          .base_and_descendants
          .alive_or_scheduled
      end
      # rubocop: enable CodeReuse/ActiveRecord

      def auto_cancel_interruptible_pipelines(pipeline_ids)
        ::Ci::Pipeline
          .id_in(pipeline_ids)
          .with_only_interruptible_builds
          .each do |cancelable_pipeline|
            Gitlab::AppLogger.info(
              class: self.class.name,
              message: "Pipeline #{pipeline.id} auto-canceling pipeline #{cancelable_pipeline.id}",
              canceled_pipeline_id: cancelable_pipeline.id,
              canceled_by_pipeline_id: pipeline.id,
              canceled_by_pipeline_source: pipeline.source
            )

            # cascade_to_children not needed because we iterate through descendants here
            ::Ci::CancelPipelineService.new(
              pipeline: cancelable_pipeline,
              current_user: nil,
              auto_canceled_by_pipeline: pipeline,
              cascade_to_children: false
            ).force_execute
          end
      end

      def pipelines_created_after
        3.days.ago
      end

      # Finding the pipelines to cancel is an expensive task that is not well
      # covered by indexes for all project use-cases and sometimes it might
      # harm other services. See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/14758
      # This feature flag is in place to disable this feature for rogue projects.
      #
      def service_disabled?
        Feature.enabled?(:disable_cancel_redundant_pipelines_service, project, type: :ops)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineCreation::CancelRedundantPipelinesService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }

  let(:prev_pipeline) { create(:ci_pipeline, project: project) }
  let!(:new_commit) { create(:commit, project: project) }
  let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }

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

  before do
    create(:ci_build, :interruptible, :running, pipeline: prev_pipeline)
    create(:ci_build, :interruptible, :success, pipeline: prev_pipeline)
    create(:ci_build, :created, pipeline: prev_pipeline)

    create(:ci_build, :interruptible, pipeline: pipeline)
  end

  shared_examples 'time limits pipeline cancellation' do
    context 'with old pipelines' do
      let(:old_pipeline) { create(:ci_pipeline, project: project, created_at: 5.days.ago) }

      before do
        create(:ci_build, :interruptible, :pending, pipeline: old_pipeline)
      end

      it 'ignores old pipelines' do
        execute

        expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled')
        expect(build_statuses(pipeline)).to contain_exactly('pending')
        expect(build_statuses(old_pipeline)).to contain_exactly('pending')
      end
    end
  end

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

    context 'when build statuses are set up correctly' do
      it 'has builds of all statuses' do
        expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
        expect(build_statuses(pipeline)).to contain_exactly('pending')
      end
    end

    context 'when auto-cancel is enabled' do
      before do
        project.update!(auto_cancel_pending_pipelines: 'enabled')
      end

      it 'cancels only previous interruptible builds' do
        execute

        expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled')
        expect(build_statuses(pipeline)).to contain_exactly('pending')
      end

      it 'logs canceled pipelines' do
        allow(Gitlab::AppLogger).to receive(:info)

        execute

        expect(Gitlab::AppLogger).to have_received(:info).with(
          class: described_class.name,
          message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}",
          canceled_pipeline_id: prev_pipeline.id,
          canceled_by_pipeline_id: pipeline.id,
          canceled_by_pipeline_source: pipeline.source
        )
      end

      context 'when the previous pipeline has a child pipeline' do
        let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) }

        context 'with another nested child pipeline' do
          let(:another_child_pipeline) { create(:ci_pipeline, child_of: child_pipeline) }

          before do
            create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
            create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline)
          end

          it 'cancels all nested child pipeline builds' do
            expect(build_statuses(another_child_pipeline)).to contain_exactly('running', 'running')

            execute

            expect(build_statuses(another_child_pipeline)).to contain_exactly('canceled', 'canceled')
          end
        end

        context 'when started after pipeline was finished' do
          before do
            create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
            prev_pipeline.update!(status: "success")
          end

          it 'cancels child pipeline builds' do
            expect(build_statuses(child_pipeline)).to contain_exactly('running')

            execute

            expect(build_statuses(child_pipeline)).to contain_exactly('canceled')
          end
        end

        context 'when the child pipeline has interruptible running jobs' do
          before do
            create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
            create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
          end

          it 'cancels all child pipeline builds' do
            expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running')

            execute

            expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
          end

          context 'when the child pipeline includes completed interruptible jobs' do
            before do
              create(:ci_build, :interruptible, :failed, pipeline: child_pipeline)
              create(:ci_build, :interruptible, :success, pipeline: child_pipeline)
            end

            it 'cancels all child pipeline builds with a cancelable_status' do
              expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success')

              execute

              expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success')
            end
          end
        end

        context 'when the child pipeline has started non-interruptible job' do
          before do
            create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
            # non-interruptible started
            create(:ci_build, :success, pipeline: child_pipeline)
          end

          it 'does not cancel any child pipeline builds' do
            expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')

            execute

            expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success')
          end
        end

        context 'when the child pipeline has non-interruptible non-started job' do
          before do
            create(:ci_build, :interruptible, :running, pipeline: child_pipeline)
          end

          not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES
          context 'when the jobs are cancelable' do
            cancelable_not_started_statuses =
              Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES)
            cancelable_not_started_statuses.each do |status|
              it "cancels all child pipeline builds when build status #{status} included" do
                # non-interruptible but non-started
                create(:ci_build, status.to_sym, pipeline: child_pipeline)

                expect(build_statuses(child_pipeline)).to contain_exactly('running', status)

                execute

                expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled')
              end
            end
          end

          context 'when the jobs are not cancelable' do
            not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES
            not_cancelable_not_started_statuses.each do |status|
              it "does not cancel child pipeline builds when build status #{status} included" do
                # non-interruptible but non-started
                create(:ci_build, status.to_sym, pipeline: child_pipeline)

                expect(build_statuses(child_pipeline)).to contain_exactly('running', status)

                execute

                expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status)
              end
            end
          end
        end
      end

      context 'when the pipeline is a child pipeline' do
        let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) }
        let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) }

        before do
          create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
          create(:ci_build, :interruptible, :running, pipeline: parent_pipeline)
        end

        it 'does not cancel any builds' do
          expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
          expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')

          execute

          expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
          expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running')
        end
      end

      context 'when the previous pipeline source is webide' do
        let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) }

        it 'does not cancel builds of the previous pipeline' do
          execute

          expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success')
          expect(build_statuses(pipeline)).to contain_exactly('pending')
        end
      end

      it 'does not cancel future pipelines' do
        expect(prev_pipeline.id).to be < pipeline.id
        expect(build_statuses(pipeline)).to contain_exactly('pending')
        expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')

        described_class.new(prev_pipeline).execute

        expect(build_statuses(pipeline.reload)).to contain_exactly('pending')
      end

      it_behaves_like 'time limits pipeline cancellation'
    end

    context 'when auto-cancel is disabled' do
      before do
        project.update!(auto_cancel_pending_pipelines: 'disabled')
      end

      it 'does not cancel any build' do
        subject

        expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
        expect(build_statuses(pipeline)).to contain_exactly('pending')
      end
    end

    context 'when enable_cancel_redundant_pipelines_service FF is enabled' do
      before do
        stub_feature_flags(disable_cancel_redundant_pipelines_service: true)
      end

      it 'does not cancel any build' do
        subject

        expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created')
        expect(build_statuses(pipeline)).to contain_exactly('pending')
      end
    end
  end

  private

  def build_statuses(pipeline)
    pipeline.builds.pluck(:status)
  end
end


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

module Ci
  module PipelineCreation
    class StartPipelineService
      attr_reader :pipeline

      def initialize(pipeline)
        @pipeline = pipeline
      end

      def execute
        ##
        # Create a persistent ref for the pipeline.
        # The pipeline ref is fetched in the jobs and deleted when the pipeline transitions to a finished state.
        pipeline.ensure_persistent_ref

        Ci::ProcessPipelineService.new(pipeline).execute
      end
    end
  end
end

::Ci::PipelineCreation::StartPipelineService.prepend_mod_with('Ci::PipelineCreation::StartPipelineService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Ci::PipelineCreation::StartPipelineService, feature_category: :continuous_integration do
  let(:pipeline) { build(:ci_pipeline) }

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

  describe '#execute' do
    it 'calls the pipeline process service' do
      expect(Ci::ProcessPipelineService)
        .to receive(:new)
        .with(pipeline)
        .and_return(double('service', execute: true))

      service.execute
    end

    it 'creates pipeline ref' do
      expect(pipeline.persistent_ref).to receive(:create).once

      service.execute
    end
  end
end


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

module Ci
  module Deployments
    class DestroyService < BaseService
      def execute(deployment)
        raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_deployment, deployment)

        return ServiceResponse.error(message: 'Cannot destroy running deployment') if deployment&.running?
        return ServiceResponse.error(message: 'Deployment currently deployed to environment') if deployment&.last?

        project.destroy_deployment_by_id(deployment)

        ServiceResponse.success(message: 'Deployment destroyed')
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ::Ci::Deployments::DestroyService, feature_category: :continuous_integration do
  let_it_be(:project) { create(:project, :repository) }

  let(:environment) { create(:environment, project: project) }
  let(:commits) { project.repository.commits(nil, { limit: 3 }) }
  let!(:deploy) do
    create(
      :deployment,
      :success,
      project: project,
      environment: environment,
      deployable: nil,
      sha: commits[2].sha
    )
  end

  let!(:running_deploy) do
    create(
      :deployment,
      :running,
      project: project,
      environment: environment,
      deployable: nil,
      sha: commits[1].sha
    )
  end

  let!(:old_deploy) do
    create(
      :deployment,
      :success,
      project: project,
      environment: environment,
      deployable: nil,
      sha: commits[0].sha,
      finished_at: 1.year.ago
    )
  end

  let(:user) { project.first_owner }

  subject { described_class.new(project, user) }

  context 'when deleting a deployment' do
    it 'delete is accepted for old deployment' do
      expect(subject.execute(old_deploy)).to be_success
    end

    it 'does not delete a running deployment' do
      response = subject.execute(running_deploy)
      expect(response).to be_an_error
      expect(response.message).to eq("Cannot destroy running deployment")
    end

    it 'does not delete the last deployment' do
      response = subject.execute(deploy)
      expect(response).to be_an_error
      expect(response.message).to eq("Deployment currently deployed to environment")
    end
  end
end


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

module Environments
  class ResetAutoStopService < ::BaseService
    def execute(environment)
      return error(_('Failed to cancel auto stop because you do not have permission to update the environment.')) unless can_update_environment?(environment)
      return error(_('Failed to cancel auto stop because the environment is not set as auto stop.')) unless environment.auto_stop_at?

      if environment.reset_auto_stop
        success
      else
        error(_('Failed to cancel auto stop because failed to update the environment.'))
      end
    end

    private

    def can_update_environment?(environment)
      can?(current_user, :update_environment, environment)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Environments::ResetAutoStopService, feature_category: :continuous_delivery do
  let_it_be(:project) { create(:project) }
  let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
  let_it_be(:reporter) { create(:user).tap { |user| project.add_reporter(user) } }

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

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

    context 'when environment will be stopped automatically' do
      let(:environment) { create(:environment, :will_auto_stop, project: project) }

      it 'resets auto stop' do
        expect(environment).to receive(:reset_auto_stop).and_call_original

        expect(subject[:status]).to eq(:success)
      end

      context 'when failed to reset auto stop' do
        before do
          expect(environment).to receive(:reset_auto_stop) { false }
        end

        it 'returns error' do
          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to eq('Failed to cancel auto stop because failed to update the environment.')
        end
      end

      context 'when user is reporter' do
        let(:user) { reporter }

        it 'returns error' do
          expect(subject[:status]).to eq(:error)
          expect(subject[:message]).to eq('Failed to cancel auto stop because you do not have permission to update the environment.')
        end
      end
    end

    context 'when environment will not be stopped automatically' do
      let(:environment) { create(:environment, project: project) }

      it 'returns error' do
        expect(subject[:status]).to eq(:error)
        expect(subject[:message]).to eq('Failed to cancel auto stop because the environment is not set as auto stop.')
      end
    end
  end
end


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

module Environments
  class ScheduleToDeleteReviewAppsService < ::BaseService
    include ::Gitlab::ExclusiveLeaseHelpers

    EXCLUSIVE_LOCK_KEY_BASE = 'environments:delete_review_apps:lock'
    LOCK_TIMEOUT = 2.minutes

    def execute
      if validation_error = validate
        return validation_error
      end

      mark_deletable_environments
    end

    private

    def key
      "#{EXCLUSIVE_LOCK_KEY_BASE}:#{project.id}"
    end

    def dry_run?
      return true if params[:dry_run].nil?

      params[:dry_run]
    end

    def validate
      return if can?(current_user, :destroy_environment, project)

      Result.new(error_message: "You do not have permission to destroy environments in this project", status: :unauthorized)
    end

    def mark_deletable_environments
      in_lock(key, ttl: LOCK_TIMEOUT, retries: 1) do
        unsafe_mark_deletable_environments
      end

    rescue FailedToObtainLockError
      Result.new(error_message: "Another process is already processing a delete request. Please retry later.", status: :conflict)
    end

    def unsafe_mark_deletable_environments
      result = Result.new
      environments = project.environments
                            .not_scheduled_for_deletion
                            .stopped_review_apps(params[:before], params[:limit])

      # Check if the actor has write permission to a potentially-protected environment.
      deletable, failed = *environments.partition { |env| current_user.can?(:destroy_environment, env) }

      if deletable.any? && failed.empty?
        mark_for_deletion(deletable) unless dry_run?
        result.set_status(:ok)
        result.set_scheduled_entries(deletable)
      else
        result.set_status(
          :bad_request,
          error_message: "No environments found for scheduled deletion. Either your query did not match any environments (default parameters match environments that are 30 days or older), or you have insufficient permissions to delete matching environments."
        )

        result.set_unprocessable_entries(failed)
      end

      result
    end

    def mark_for_deletion(deletable_environments)
      Environment.id_in(deletable_environments).schedule_to_delete
    end

    class Result
      attr_accessor :scheduled_entries, :unprocessable_entries, :error_message, :status

      def initialize(scheduled_entries: [], unprocessable_entries: [], error_message: nil, status: nil)
        self.scheduled_entries = scheduled_entries
        self.unprocessable_entries = unprocessable_entries
        self.error_message = error_message
        self.status = status
      end

      def success?
        status == :ok
      end

      def set_status(status, error_message: nil)
        self.status = status
        self.error_message = error_message
      end

      def set_scheduled_entries(entries)
        self.scheduled_entries = entries
      end

      def set_unprocessable_entries(entries)
        self.unprocessable_entries = entries
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Environments::ScheduleToDeleteReviewAppsService, feature_category: :continuous_delivery do
  include ExclusiveLeaseHelpers

  let_it_be(:maintainer) { create(:user) }
  let_it_be(:developer)  { create(:user) }
  let_it_be(:reporter)   { create(:user) }
  let_it_be(:project)    { create(:project, :private, :repository, namespace: maintainer.namespace) }

  let(:service)      { described_class.new(project, current_user, before: 30.days.ago, dry_run: dry_run) }
  let(:dry_run)      { false }
  let(:current_user) { maintainer }

  before do
    project.add_maintainer(maintainer)
    project.add_developer(developer)
    project.add_reporter(reporter)
  end

  describe "#execute" do
    subject { service.execute }

    shared_examples "can schedule for deletion" do
      let!(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
      let!(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) }
      let!(:old_active_review_env)  { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) }
      let!(:old_stopped_other_env)  { create(:environment, :stopped, created_at: 31.days.ago, project: project) }
      let!(:new_stopped_other_env)  { create(:environment, :stopped, project: project) }
      let!(:old_active_other_env)   { create(:environment, :available, created_at: 31.days.ago, project: project) }
      let!(:already_deleting_env)   { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project, auto_delete_at: 1.day.from_now) }
      let(:already_deleting_time)   { already_deleting_env.reload.auto_delete_at }

      context "live run" do
        let(:dry_run) { false }

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

        it "marks the correct environment as scheduled_entries" do
          expect(subject.success?).to be_truthy
          expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
          expect(subject.unprocessable_entries).to be_empty

          old_stopped_review_env.reload
          new_stopped_review_env.reload
          old_active_review_env.reload
          old_stopped_other_env.reload
          new_stopped_other_env.reload
          old_active_other_env.reload
          already_deleting_env.reload

          expect(old_stopped_review_env.auto_delete_at).to eq(1.week.from_now)
          expect(new_stopped_review_env.auto_delete_at).to be_nil
          expect(old_active_review_env.auto_delete_at).to be_nil
          expect(old_stopped_other_env.auto_delete_at).to be_nil
          expect(new_stopped_other_env.auto_delete_at).to be_nil
          expect(old_active_other_env.auto_delete_at).to be_nil
          expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
        end
      end

      context "dry run" do
        let(:dry_run) { true }

        it "returns the same but doesn't update the record" do
          expect(subject.success?).to be_truthy
          expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
          expect(subject.unprocessable_entries).to be_empty

          old_stopped_review_env.reload
          new_stopped_review_env.reload
          old_active_review_env.reload
          old_stopped_other_env.reload
          new_stopped_other_env.reload
          old_active_other_env.reload
          already_deleting_env.reload

          expect(old_stopped_review_env.auto_delete_at).to be_nil
          expect(new_stopped_review_env.auto_delete_at).to be_nil
          expect(old_active_review_env.auto_delete_at).to be_nil
          expect(old_stopped_other_env.auto_delete_at).to be_nil
          expect(new_stopped_other_env.auto_delete_at).to be_nil
          expect(old_active_other_env.auto_delete_at).to be_nil
          expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
        end
      end

      describe "execution in parallel" do
        before do
          stub_exclusive_lease_taken(service.send(:key))
        end

        it "does not execute unsafe_mark_scheduled_entries_environments" do
          expect(service).not_to receive(:unsafe_mark_scheduled_entries_environments)

          expect(subject.success?).to be_falsey
          expect(subject.status).to eq(:conflict)
        end
      end
    end

    context "as a maintainer" do
      let(:current_user) { maintainer }

      it_behaves_like "can schedule for deletion"
    end

    context "as a developer" do
      let(:current_user) { developer }

      it_behaves_like "can schedule for deletion"
    end

    context "as a reporter" do
      let(:current_user) { reporter }

      it "fails to delete environments" do
        old_stopped_review_env = create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project)

        expect(subject.success?).to be_falsey

        # Both of these should be empty as we fail before testing them
        expect(subject.scheduled_entries).to be_empty
        expect(subject.unprocessable_entries).to be_empty

        old_stopped_review_env.reload

        expect(old_stopped_review_env.auto_delete_at).to be_nil
      end
    end
  end
end


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

module Environments
  # This class creates an environment record for a pipeline job.
  class CreateForJobService
    def execute(job)
      return unless job.is_a?(::Ci::Processable) && job.has_environment_keyword?

      environment = to_resource(job)

      if environment.persisted?
        job.persisted_environment = environment
        job.assign_attributes(metadata_attributes: { expanded_environment_name: environment.name })
      else
        job.assign_attributes(status: :failed, failure_reason: :environment_creation_failure)
      end

      environment
    end

    private

    # rubocop: disable Performance/ActiveRecordSubtransactionMethods
    def to_resource(job)
      job.project.environments.safe_find_or_create_by(name: job.expanded_environment_name) do |environment|
        # Initialize the attributes at creation
        environment.auto_stop_in = expanded_auto_stop_in(job)
        environment.tier = job.environment_tier_from_options
        environment.merge_request = job.pipeline.merge_request
      end
    end
    # rubocop: enable Performance/ActiveRecordSubtransactionMethods

    def expanded_auto_stop_in(job)
      return unless job.environment_auto_stop_in

      ExpandVariables.expand(job.environment_auto_stop_in, -> { job.simple_variables.sort_and_expand_all })
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Environments::CreateForJobService, feature_category: :continuous_delivery do
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { create(:user) }
  let_it_be(:pipeline) { create(:ci_pipeline, project: project) }

  let(:service) { described_class.new }

  it_behaves_like 'create environment for job' do
    let(:factory_type) { :ci_build }
  end

  it_behaves_like 'create environment for job' do
    let(:factory_type) { :ci_bridge }
  end
end


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

module Environments
  class AutoStopService
    include ::Gitlab::ExclusiveLeaseHelpers
    include ::Gitlab::LoopHelpers

    BATCH_SIZE = 100
    LOOP_TIMEOUT = 45.minutes
    LOOP_LIMIT = 1000
    EXCLUSIVE_LOCK_KEY = 'environments:auto_stop:lock'
    LOCK_TIMEOUT = 50.minutes

    ##
    # Stop expired environments on GitLab instance
    #
    # This auto stop process cannot run for more than 45 minutes. This is for
    # preventing multiple `AutoStopCronWorker` CRON jobs run concurrently,
    # which is scheduled at every hour.
    def execute
      in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
        loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
          stop_in_batch
        end
      end
    end

    private

    def stop_in_batch
      environments = Environment.preload_project.select(:id, :project_id).auto_stoppable(BATCH_SIZE)

      return false if environments.empty?

      Environments::AutoStopWorker.bulk_perform_async_with_contexts(
        environments,
        arguments_proc: -> (environment) { environment.id },
        context_proc: -> (environment) { { project: environment.project } }
      )

      true
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Environments::AutoStopService, :clean_gitlab_redis_shared_state, :sidekiq_inline,
  feature_category: :continuous_delivery do
  include CreateEnvironmentsHelpers
  include ExclusiveLeaseHelpers

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

  let(:service) { described_class.new }

  before_all do
    project.add_developer(user)
  end

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

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

    let(:environments) { Environment.all }

    before_all do
      project.add_developer(user)
      project.repository.add_branch(user, 'review/feature-1', 'master')
      project.repository.add_branch(user, 'review/feature-2', 'master')
    end

    before do
      create_review_app(user, project, 'review/feature-1')
      create_review_app(user, project, 'review/feature-2')
    end

    it 'stops environments and play stop jobs' do
      expect { subject }
        .to change { Environment.all.map(&:state).uniq }
        .from(['available']).to(['stopping'])

      expect(Ci::Build.where(name: 'stop_review_app').map(&:status).uniq).to eq(['pending'])
    end

    it 'schedules stop processes in bulk' do
      args = [[Environment.find_by_name('review/feature-1').id], [Environment.find_by_name('review/feature-2').id]]

      expect(Environments::AutoStopWorker)
        .to receive(:bulk_perform_async).with(args).once.and_call_original

      subject
    end

    context 'when the other sidekiq worker has already been running' do
      before do
        stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY)
      end

      it 'does not execute stop_in_batch' do
        expect_next_instance_of(described_class) do |service|
          expect(service).not_to receive(:stop_in_batch)
        end

        expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
      end
    end

    context 'when loop reached timeout' do
      before do
        stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds)
        stub_const("#{described_class}::LOOP_LIMIT", 100_000)
        allow_next_instance_of(described_class) do |service|
          allow(service).to receive(:stop_in_batch) { true }
        end
      end

      it 'returns false and does not continue the process' do
        is_expected.to eq(false)
      end
    end

    context 'when loop reached loop limit' do
      before do
        stub_const("#{described_class}::LOOP_LIMIT", 1)
        stub_const("#{described_class}::BATCH_SIZE", 1)
      end

      it 'stops only one available environment' do
        expect { subject }.to change { Environment.available.count }.by(-1)
      end
    end
  end
end