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

module Releases
  class CreateService < Releases::BaseService
    def execute
      return error(_('Access Denied'), 403) unless allowed?
      return error(_('You are not allowed to create this tag as it is protected.'), 403) unless can_create_tag?
      return error(_('Release already exists'), 409) if release
      return error(format(_("Milestone(s) not found: %{milestones}"), milestones: inexistent_milestone_titles.join(', ')), 400) if inexistent_milestone_titles.any? # rubocop:disable Layout/LineLength
      return error(format(_("Milestone id(s) not found: %{milestones}"), milestones: inexistent_milestone_ids.join(', ')), 400) if inexistent_milestone_ids.any? # rubocop:disable Layout/LineLength

      # should be found before the creation of new tag
      # because tag creation can spawn new pipeline
      # which won't have any data for evidence yet
      evidence_pipeline = Releases::EvidencePipelineFinder.new(project, params).execute

      tag = ensure_tag

      return tag unless tag.is_a?(Gitlab::Git::Tag)

      create_release(tag, evidence_pipeline)
    end

    private

    def ensure_tag
      existing_tag || create_tag
    end

    def create_tag
      return error('Ref is not specified', 422) unless ref

      result = Tags::CreateService
        .new(project, current_user)
        .execute(tag_name, ref, tag_message)

      return result unless result[:status] == :success

      result[:tag]
    end

    def allowed?
      Ability.allowed?(current_user, :create_release, project)
    end

    def can_create_tag?
      ::Gitlab::UserAccess.new(current_user, container: project).can_create_tag?(tag_name)
    end

    def create_release(tag, evidence_pipeline)
      release = build_release(tag)

      if project.catalog_resource && release.valid?
        response = Ci::Catalog::Resources::ReleaseService.new(release).execute

        return error(response.message, 422) if response.error?
      end

      release.save!

      notify_create_release(release)

      execute_hooks(release, 'create')

      create_evidence!(release, evidence_pipeline)

      audit(release, action: :created)

      success(tag: tag, release: release)
    rescue StandardError => e
      error(e.message, 400)
    end

    def notify_create_release(release)
      NotificationService.new.async.send_new_release_notifications(release)
    end

    def build_release(tag)
      project.releases.build(
        name: name,
        description: description,
        author: current_user,
        tag: tag.name,
        sha: tag.dereferenced_target.sha,
        released_at: released_at,
        links_attributes: links_attributes,
        milestones: milestones
      )
    end

    def links_attributes
      (params.dig(:assets, 'links') || []).map do |link_params|
        Releases::Links::Params.new(link_params).allowed_params
      end
    end

    def create_evidence!(release, pipeline)
      return if release.historical_release? || release.upcoming_release?

      ::Releases::CreateEvidenceWorker.perform_async(release.id, pipeline&.id)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Releases::CreateService, feature_category: :continuous_integration do
  let(:project) { create(:project, :repository) }
  let(:user) { create(:user) }
  let(:tag_name) { project.repository.tag_names.first }
  let(:tag_message) { nil }
  let(:tag_sha) { project.repository.find_tag(tag_name).dereferenced_target.sha }
  let(:name) { 'Bionic Beaver' }
  let(:description) { 'Awesome release!' }
  let(:params) { { tag: tag_name, name: name, description: description, ref: ref, tag_message: tag_message } }
  let(:ref) { nil }
  let(:service) { described_class.new(project, user, params) }

  before do
    project.add_maintainer(user)
  end

  describe '#execute' do
    shared_examples 'a successful release creation' do
      it 'creates a new release' do
        expected_job_count = MailScheduler::NotificationServiceWorker.jobs.size + 1

        expect_next_instance_of(Release) do |release|
          expect(release)
            .to receive(:execute_hooks)
            .with('create')
        end

        result = service.execute

        expect(project.releases.count).to eq(1)
        expect(result[:status]).to eq(:success)
        expect(result[:tag]).not_to be_nil
        expect(result[:release]).not_to be_nil
        expect(result[:release].description).to eq(description)
        expect(result[:release].name).to eq(name)
        expect(result[:release].author).to eq(user)
        expect(result[:release].sha).to eq(tag_sha)
        expect(MailScheduler::NotificationServiceWorker.jobs.size).to eq(expected_job_count)
      end
    end

    it_behaves_like 'a successful release creation'

    context 'when the tag does not exist' do
      let(:tag_name) { 'non-exist-tag' }

      it 'raises an error' do
        result = service.execute

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

    context 'when project is a catalog resource' do
      let(:project) { create(:project, :catalog_resource_with_components, create_tag: 'final') }
      let!(:ci_catalog_resource) { create(:ci_catalog_resource, project: project) }
      let(:ref) { 'master' }

      context 'and it is valid' do
        it_behaves_like 'a successful release creation'
      end

      context 'and it is an invalid resource' do
        let_it_be(:project) { create(:project, :repository) }

        it 'raises an error and does not update the release' do
          result = service.execute

          expect(result[:status]).to eq(:error)
          expect(result[:http_status]).to eq(422)
          expect(result[:message]).to eq(
            'Project must have a description, ' \
            'Project must contain components. Ensure you are using the correct directory structure')
        end
      end
    end

    context 'when ref is provided' do
      let(:ref) { 'master' }
      let(:tag_name) { 'foobar' }

      it_behaves_like 'a successful release creation'

      it 'creates a tag if the tag does not exist' do
        expect(project.repository.ref_exists?("refs/tags/#{tag_name}")).to be_falsey

        result = service.execute
        expect(result[:status]).to eq(:success)
        expect(result[:tag]).not_to be_nil
        expect(result[:release]).not_to be_nil
      end

      context 'and the tag would be protected' do
        let!(:protected_tag) { create(:protected_tag, project: project, name: tag_name) }

        context 'and the user does not have permissions' do
          let(:user) { create(:user) }

          before do
            project.add_developer(user)
          end

          it 'raises an error' do
            result = service.execute

            expect(result[:status]).to eq(:error)
            expect(result[:http_status]).to eq(403)
          end
        end

        context 'and the user has permissions' do
          it_behaves_like 'a successful release creation'
        end
      end

      context 'and tag_message is provided' do
        let(:ref) { 'master' }
        let(:tag_name) { 'foobar' }
        let(:tag_message) { 'Annotated tag message' }

        it_behaves_like 'a successful release creation'

        it 'creates a tag if the tag does not exist' do
          expect(project.repository.ref_exists?("refs/tags/#{tag_name}")).to be_falsey

          result = service.execute
          expect(result[:status]).to eq(:success)
          expect(result[:tag]).not_to be_nil
          expect(result[:release]).not_to be_nil
          expect(project.repository.find_tag(tag_name).message).to eq(tag_message)
        end
      end
    end

    context 'there already exists a release on a tag' do
      let!(:release) do
        create(:release, project: project, tag: tag_name, description: description)
      end

      it 'raises an error and does not update the release' do
        result = service.execute
        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(409)
        expect(project.releases.find_by(tag: tag_name).description).to eq(description)
      end
    end

    context 'when a passed-in milestone does not exist for this project' do
      it 'raises an error saying the milestone is inexistent' do
        inexistent_milestone_tag = 'v111.0'
        service = described_class.new(project, user, params.merge!({ milestones: [inexistent_milestone_tag] }))
        result = service.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(400)
        expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}")
      end

      it 'raises an error saying the milestone id is inexistent' do
        inexistent_milestone_id = non_existing_record_id
        service = described_class.new(project, user, params.merge!({ milestone_ids: [inexistent_milestone_id] }))
        result = service.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(400)
        expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}")
      end
    end

    context 'when existing milestone is passed in' do
      let(:title) { 'v1.0' }
      let(:milestone) { create(:milestone, :active, project: project, title: title) }
      let(:params_with_milestone) { params.merge!({ milestones: [title] }) }
      let(:service) { described_class.new(milestone.project, user, params_with_milestone) }

      shared_examples 'creates release' do
        it 'creates a release and ties this milestone to it' do
          result = service.execute

          expect(project.releases.count).to eq(1)
          expect(result[:status]).to eq(:success)

          release = project.releases.last

          expect(release.milestones).to match_array([milestone])
        end
      end

      context 'by title' do
        it_behaves_like 'creates release'
      end

      context 'by ids' do
        let(:params_with_milestone) { params.merge!({ milestone_ids: [milestone.id] }) }

        it_behaves_like 'creates release'
      end

      context 'when another release was previously created with that same milestone linked' do
        it 'also creates another release tied to that same milestone' do
          other_release = create(:release, milestones: [milestone], project: project, tag: 'v1.0')
          service.execute
          release = project.releases.last

          expect(release.milestones).to match_array([milestone])
          expect(other_release.milestones).to match_array([milestone])
          expect(release.id).not_to eq(other_release.id)
        end
      end
    end

    context 'when multiple existing milestones are passed in' do
      let(:title_1) { 'v1.0' }
      let(:title_2) { 'v1.0-rc' }
      let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
      let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }

      shared_examples 'creates multiple releases' do
        it 'creates a release and ties it to these milestones' do
          described_class.new(project, user, params_with_milestones).execute
          release = project.releases.last

          expect(release.milestones.map(&:title)).to include(title_1, title_2)
        end
      end

      context 'by title' do
        let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) }

        it_behaves_like 'creates multiple releases'
      end

      context 'by ids' do
        let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone_1.id, milestone_2.id] }) }

        it_behaves_like 'creates multiple releases'
      end
    end

    context 'when multiple milestone titles are passed in but one of them does not exist' do
      let(:title) { 'v1.0' }
      let(:inexistent_title) { 'v111.0' }
      let!(:milestone) { create(:milestone, :active, project: project, title: title) }
      let!(:params_with_milestones) { params.merge!({ milestones: [title, inexistent_title] }) }
      let(:service) { described_class.new(milestone.project, user, params_with_milestones) }

      it 'raises an error' do
        result = service.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(400)
        expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_title}")
      end

      it 'does not create any release' do
        expect do
          service.execute
        end.not_to change(Release, :count)
      end

      context 'with milestones as ids' do
        let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone.id, non_existing_record_id] }) }

        it 'raises an error' do
          result = service.execute

          expect(result[:status]).to eq(:error)
          expect(result[:http_status]).to eq(400)
          expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}")
        end
      end
    end

    context 'no milestone association behavior' do
      let(:title_1) { 'v1.0' }
      let(:title_2) { 'v1.0-rc' }
      let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) }
      let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) }

      context 'when no milestones parameter is passed' do
        it 'creates a release without a milestone tied to it' do
          expect(service.param_for_milestone_titles_provided?).to be_falsey

          service.execute
          release = project.releases.last

          expect(release.milestones).to be_empty
        end

        it 'does not create any new MilestoneRelease object' do
          expect { service.execute }.not_to change { MilestoneRelease.count }
        end
      end

      context 'when an empty array is passed as the milestones parameter' do
        it 'creates a release without a milestone tied to it' do
          service = described_class.new(project, user, params.merge!({ milestones: [] }))
          service.execute
          release = project.releases.last

          expect(release.milestones).to be_empty
        end
      end

      context 'when nil is passed as the milestones parameter' do
        it 'creates a release without a milestone tied to it' do
          expect(service.param_for_milestone_titles_provided?).to be_falsey

          service = described_class.new(project, user, params.merge!({ milestones: nil }))
          service.execute
          release = project.releases.last

          expect(release.milestones).to be_empty
        end
      end
    end
  end

  context 'Evidence collection' do
    let(:sha) { project.repository.commit('master').sha }
    let(:params) do
      {
        name: 'New release',
        ref: 'master',
        tag: 'v0.1',
        description: 'Super nice release',
        released_at: released_at
      }.compact
    end

    let(:last_release) { project.releases.last }

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

    subject { service.execute }

    context 'historical release' do
      let(:released_at) { 3.weeks.ago }

      it 'does not execute CreateEvidenceWorker' do
        expect { subject }.not_to change(Releases::CreateEvidenceWorker.jobs, :size)
      end

      it 'does not create an Evidence object', :sidekiq_inline do
        expect { subject }.not_to change(Releases::Evidence, :count)
      end

      it 'is a historical release' do
        subject

        expect(last_release.historical_release?).to be_truthy
      end

      it 'is not an upcoming release' do
        subject

        expect(last_release.upcoming_release?).to be_falsy
      end
    end

    shared_examples 'uses the right pipeline for evidence' do
      it 'creates evidence without pipeline if it does not exist', :sidekiq_inline do
        expect_next_instance_of(Releases::CreateEvidenceService, anything, pipeline: nil) do |service|
          expect(service).to receive(:execute).and_call_original
        end

        expect { subject }.to change(Releases::Evidence, :count).by(1)
      end

      it 'uses the last pipeline for evidence', :sidekiq_inline do
        create(:ci_empty_pipeline, sha: sha, project: project) # old pipeline
        pipeline = create(:ci_empty_pipeline, sha: sha, project: project)

        expect_next_instance_of(Releases::CreateEvidenceService, anything, pipeline: pipeline) do |service|
          expect(service).to receive(:execute).and_call_original
        end

        expect { subject }.to change(Releases::Evidence, :count).by(1)
      end

      context 'when old evidence_pipeline is passed to service' do
        let!(:old_pipeline) { create(:ci_empty_pipeline, sha: sha, project: project) }
        let!(:new_pipeline) { create(:ci_empty_pipeline, sha: sha, project: project) }
        let(:params) do
          super().merge(
            evidence_pipeline: old_pipeline
          )
        end

        it 'uses the old pipeline for evidence', :sidekiq_inline do
          expect_next_instance_of(Releases::CreateEvidenceService, anything, pipeline: old_pipeline) do |service|
            expect(service).to receive(:execute).and_call_original
          end

          expect { subject }.to change(Releases::Evidence, :count).by(1)
        end
      end

      it 'pipeline is still being used for evidence if new pipeline is being created for tag', :sidekiq_inline do
        pipeline = create(:ci_empty_pipeline, sha: sha, project: project)

        expect(project.repository).to receive(:add_tag).and_wrap_original do |m, *args|
          create(:ci_empty_pipeline, sha: sha, project: project)
          m.call(*args)
        end

        expect_next_instance_of(Releases::CreateEvidenceService, anything, pipeline: pipeline) do |service|
          expect(service).to receive(:execute).and_call_original
        end

        expect { subject }.to change(Releases::Evidence, :count).by(1)
      end

      it 'uses the last pipeline for evidence when tag is already created', :sidekiq_inline do
        Tags::CreateService.new(project, user).execute('v0.1', 'master', nil)

        expect(project.repository.find_tag('v0.1')).to be_present

        create(:ci_empty_pipeline, sha: sha, project: project) # old pipeline
        pipeline = create(:ci_empty_pipeline, sha: sha, project: project)

        expect_next_instance_of(Releases::CreateEvidenceService, anything, pipeline: pipeline) do |service|
          expect(service).to receive(:execute).and_call_original
        end

        expect { subject }.to change(Releases::Evidence, :count).by(1)
      end
    end

    context 'immediate release' do
      let(:released_at) { nil }

      it 'sets `released_at` to the current dttm' do
        subject

        expect(last_release.updated_at).to be_like_time(Time.current)
      end

      it 'queues CreateEvidenceWorker' do
        expect { subject }.to change(Releases::CreateEvidenceWorker.jobs, :size).by(1)
      end

      it 'creates Evidence', :sidekiq_inline do
        expect { subject }.to change(Releases::Evidence, :count).by(1)
      end

      it 'is not a historical release' do
        subject

        expect(last_release.historical_release?).to be_falsy
      end

      it 'is not an upcoming release' do
        subject

        expect(last_release.upcoming_release?).to be_falsy
      end

      include_examples 'uses the right pipeline for evidence'
    end

    context 'upcoming release' do
      let(:released_at) { 1.day.from_now }

      it 'does not execute CreateEvidenceWorker' do
        expect { subject }.not_to change(Releases::CreateEvidenceWorker.jobs, :size)
      end

      it 'does not create an Evidence object', :sidekiq_inline do
        expect { subject }.not_to change(Releases::Evidence, :count)
      end

      it 'is not a historical release' do
        subject

        expect(last_release.historical_release?).to be_falsy
      end

      it 'is an upcoming release' do
        subject

        expect(last_release.upcoming_release?).to be_truthy
      end
    end
  end
end


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

module Releases
  module Links
    class DestroyService < BaseService
      def execute(link)
        return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?
        return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link

        if link.destroy
          ServiceResponse.success(payload: { link: link })
        else
          ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
        end
      end

      private

      def allowed?
        Ability.allowed?(current_user, :destroy_release, release)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Releases::Links::DestroyService, feature_category: :release_orchestration do
  let(:service) { described_class.new(release, user, {}) }
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { create(:user) }
  let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }

  let!(:release_link) do
    create(
      :release_link,
      release: release,
      name: 'awesome-app.dmg',
      url: 'https://example.com/download/awesome-app.dmg'
    )
  end

  before do
    project.add_developer(user)
  end

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

    it 'successfully deletes a release link' do
      expect { execute }.to change { release.links.count }.by(-1)

      is_expected.to be_success
    end

    context 'when user does not have access to delete release link' do
      before do
        project.add_guest(user)
      end

      it 'returns an error' do
        expect { execute }.not_to change { release.links.count }

        is_expected.to be_error
        expect(execute.message).to include('Access Denied')
        expect(execute.reason).to eq(:forbidden)
      end
    end

    context 'when release link does not exist' do
      let(:release_link) { nil }

      it 'returns an error' do
        expect { execute }.not_to change { release.links.count }

        is_expected.to be_error
        expect(execute.message).to eq('Link does not exist')
        expect(execute.reason).to eq(:not_found)
      end
    end

    context 'when release link deletion failed' do
      before do
        allow(release_link).to receive(:destroy).and_return(false)
      end

      it 'returns an error' do
        expect { execute }.not_to change { release.links.count }

        is_expected.to be_error
        expect(execute.reason).to eq(:bad_request)
      end
    end
  end
end


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

module Releases
  module Links
    class UpdateService < BaseService
      def execute(link)
        return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?
        return ServiceResponse.error(reason: REASON_NOT_FOUND, message: _('Link does not exist')) unless link

        if link.update(allowed_params)
          ServiceResponse.success(payload: { link: link })
        else
          ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
        end
      end

      private

      def allowed?
        Ability.allowed?(current_user, :update_release, release)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Releases::Links::UpdateService, feature_category: :release_orchestration do
  let(:service) { described_class.new(release, user, params) }
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { create(:user) }
  let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }

  let(:release_link) do
    create(
      :release_link,
      release: release,
      name: 'awesome-app.dmg',
      url: 'https://example.com/download/awesome-app.dmg'
    )
  end

  let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
  let(:name) { 'link' }
  let(:url) { 'https://example.com' }
  let(:direct_asset_path) { '/path' }
  let(:link_type) { 'other' }

  before do
    project.add_developer(user)
  end

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

    let(:updated_link) { execute.payload[:link] }

    it 'successfully updates a release link' do
      is_expected.to be_success

      expect(updated_link).to have_attributes(
        name: name,
        url: url,
        filepath: direct_asset_path,
        link_type: link_type
      )
    end

    context 'when user does not have access to update release link' do
      before do
        project.add_guest(user)
      end

      it 'returns an error' do
        is_expected.to be_error
        expect(execute.message).to include('Access Denied')
        expect(execute.reason).to eq(:forbidden)
      end
    end

    context 'when url is invalid' do
      let(:url) { 'not_a_url' }

      it 'returns an error' do
        is_expected.to be_error
        expect(execute.message[0]).to include('Url is blocked')
        expect(execute.reason).to eq(:bad_request)
      end
    end

    context 'when both direct_asset_path and filepath are provided' do
      let(:params) { super().merge(filepath: '/filepath') }

      it 'prefers direct_asset_path' do
        is_expected.to be_success

        expect(updated_link.filepath).to eq(direct_asset_path)
      end
    end

    context 'when only filepath is set' do
      let(:params) { super().merge(filepath: '/filepath') }
      let(:direct_asset_path) { nil }

      it 'uses filepath' do
        is_expected.to be_success

        expect(updated_link.filepath).to eq('/filepath')
      end
    end
  end
end


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

module Releases
  module Links
    class CreateService < BaseService
      def execute
        return ServiceResponse.error(reason: REASON_FORBIDDEN, message: _('Access Denied')) unless allowed?

        link = release.links.create(allowed_params)

        if link.persisted?
          ServiceResponse.success(payload: { link: link })
        else
          ServiceResponse.error(reason: REASON_BAD_REQUEST, message: link.errors.full_messages)
        end
      end

      private

      def allowed?
        Ability.allowed?(current_user, :create_release, release)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Releases::Links::CreateService, feature_category: :release_orchestration do
  let(:service) { described_class.new(release, user, params) }
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { create(:user) }
  let_it_be(:release) { create(:release, project: project, author: user, tag: 'v1.1.0') }

  let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type } }
  let(:name) { 'link' }
  let(:url) { 'https://example.com' }
  let(:direct_asset_path) { '/path' }
  let(:link_type) { 'other' }

  before do
    project.add_developer(user)
  end

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

    let(:link) { subject.payload[:link] }

    it 'successfully creates a release link' do
      expect { execute }.to change { Releases::Link.count }.by(1)

      expect(link).to have_attributes(
        name: name,
        url: url,
        filepath: direct_asset_path,
        link_type: link_type
      )
    end

    context 'when user does not have access to create release link' do
      before do
        project.add_guest(user)
      end

      it 'returns an error' do
        expect { execute }.not_to change { Releases::Link.count }

        is_expected.to be_error
        expect(execute.message).to include('Access Denied')
        expect(execute.reason).to eq(:forbidden)
      end
    end

    context 'when url is invalid' do
      let(:url) { 'not_a_url' }

      it 'returns an error' do
        expect { execute }.not_to change { Releases::Link.count }

        is_expected.to be_error
        expect(execute.message[0]).to include('Url is blocked')
        expect(execute.reason).to eq(:bad_request)
      end
    end

    context 'when both direct_asset_path and filepath are provided' do
      let(:params) { super().merge(filepath: '/filepath') }

      it 'prefers direct_asset_path' do
        is_expected.to be_success

        expect(link.filepath).to eq(direct_asset_path)
      end
    end

    context 'when only filepath is set' do
      let(:params) { super().merge(filepath: '/filepath') }
      let(:direct_asset_path) { nil }

      it 'uses filepath' do
        is_expected.to be_success

        expect(link.filepath).to eq('/filepath')
      end
    end
  end
end


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

module Releases
  module Links
    class Params
      def initialize(params)
        @params = params.with_indifferent_access
      end

      def allowed_params
        @allowed_params ||= params.slice(:name, :url, :link_type).tap do |hash|
          hash[:filepath] = filepath if provided_filepath?
        end
      end

      private

      attr_reader :params

      def provided_filepath?
        params.key?(:direct_asset_path) || params.key?(:filepath)
      end

      def filepath
        params[:direct_asset_path] || params[:filepath]
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Releases::Links::Params, feature_category: :release_orchestration do
  subject(:filter) { described_class.new(params) }

  let(:params) { { name: name, url: url, direct_asset_path: direct_asset_path, link_type: link_type, unknown: '?' } }
  let(:name) { 'link' }
  let(:url) { 'https://example.com' }
  let(:direct_asset_path) { '/path' }
  let(:link_type) { 'other' }

  describe '#allowed_params' do
    subject { filter.allowed_params }

    it 'returns only allowed params' do
      is_expected.to eq('name' => name, 'url' => url, 'filepath' => direct_asset_path, 'link_type' => link_type)
    end

    context 'when deprecated filepath is used' do
      let(:params) { super().merge(direct_asset_path: nil, filepath: 'filepath') }

      it 'uses filepath value' do
        is_expected.to eq('name' => name, 'url' => url, 'filepath' => 'filepath', 'link_type' => link_type)
      end
    end

    context 'when both direct_asset_path and filepath are provided' do
      let(:params) { super().merge(filepath: 'filepath') }

      it 'uses direct_asset_path value' do
        is_expected.to eq('name' => name, 'url' => url, 'filepath' => direct_asset_path, 'link_type' => link_type)
      end
    end
  end
end


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

module ChatNames
  class FindUserService
    def initialize(team_id, user_id)
      @team_id = team_id
      @user_id = user_id
    end

    def execute
      chat_name = find_chat_name
      return unless chat_name

      record_chat_activity(chat_name)
      chat_name
    end

    private

    attr_reader :team_id, :user_id

    # rubocop: disable CodeReuse/ActiveRecord
    def find_chat_name
      ChatName.find_by(
        team_id: team_id,
        chat_id: user_id
      )
    end
    # rubocop: enable CodeReuse/ActiveRecord

    def record_chat_activity(chat_name)
      chat_name.update_last_used_at
      Users::ActivityService.new(author: chat_name.user).execute
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state, feature_category: :user_profile do
  describe '#execute' do
    subject { described_class.new(team_id, user_id).execute }

    context 'find user mapping' do
      let_it_be(:user) { create(:user) }
      let(:chat_name) { create(:chat_name, user: user) }

      let(:team_id) { chat_name.team_id }
      let(:user_id) { chat_name.chat_id }

      context 'when existing user is requested' do
        it 'returns the existing chat_name' do
          is_expected.to eq(chat_name)
        end

        it 'updates the last used timestamp if one is not already set' do
          expect { subject }.to change { chat_name.reload.last_used_at }.from(nil)
        end

        it 'only updates an existing timestamp once within a certain time frame' do
          expect { described_class.new(team_id, user_id).execute }.to change { chat_name.reload.last_used_at }.from(nil)
          expect { described_class.new(team_id, user_id).execute }.not_to change { chat_name.reload.last_used_at }
        end

        it 'records activity for the related user' do
          expect_next_instance_of(Users::ActivityService, author: user) do |activity_service|
            expect(activity_service).to receive(:execute)
          end

          subject
        end
      end

      context 'when different user is requested' do
        let(:user_id) { 'non-existing-user' }

        it 'returns nil' do
          is_expected.to be_nil
        end
      end
    end
  end
end


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

module ChatNames
  class AuthorizeUserService
    include Gitlab::Routing

    def initialize(params)
      @params = params
    end

    def execute
      return unless chat_name_params.values.all?(&:present?)

      token = request_token

      new_profile_chat_name_url(token: token) if token
    end

    private

    def request_token
      chat_name_token.store!(chat_name_params)
    end

    def chat_name_token
      @chat_name_token ||= Gitlab::ChatNameToken.new
    end

    def chat_name_params
      {
        team_id: @params[:team_id],
        team_domain: @params[:team_domain],
        chat_id: @params[:user_id],
        chat_name: @params[:user_name]
      }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ChatNames::AuthorizeUserService, feature_category: :user_profile do
  describe '#execute' do
    let(:result) { subject.execute }

    subject { described_class.new(params) }

    context 'when all parameters are valid' do
      let(:params) { { team_id: 'T0001', team_domain: 'myteam', user_id: 'U0001', user_name: 'user' } }

      it 'produces a valid HTTP URL' do
        expect(result).to be_http_url
      end

      it 'requests a new token' do
        expect(subject).to receive(:request_token).once.and_call_original

        subject.execute
      end
    end

    context 'when there are missing parameters' do
      let(:params) { {} }

      it 'does not produce a URL' do
        expect(result).to be_nil
      end

      it 'does not request a new token' do
        expect(subject).not_to receive(:request_token)

        subject.execute
      end
    end
  end
end


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

module ErrorTracking
  class IssueDetailsService < ErrorTracking::BaseService
    include Gitlab::Routing
    include Gitlab::Utils::StrongMemoize

    private

    def perform
      response = find_issue_details(params[:issue_id])

      compose_response(response) do
        # The gitlab_issue attribute can contain an absolute GitLab url from the Sentry Client
        # here we overwrite that in favor of our own data if we have it
        response[:issue].gitlab_issue = gitlab_issue_url if gitlab_issue_url
      end
    end

    def gitlab_issue_url
      strong_memoize(:gitlab_issue_url) do
        # Use the absolute url to match the GitLab issue url that the Sentry api provides
        project_issue_url(project, gitlab_issue.iid) if gitlab_issue
      end
    end

    def gitlab_issue
      strong_memoize(:gitlab_issue) do
        SentryIssueFinder
          .new(project, current_user: current_user)
          .execute(params[:issue_id])
          &.issue
      end
    end

    def parse_response(response)
      { issue: response[:issue] }
    end

    def find_issue_details(issue_id)
      # There are 2 types of the data source for the error tracking feature:
      #
      # * When integrated error tracking is enabled, we use the application database
      #   to read and save error tracking data.
      #
      # * When integrated error tracking is disabled we call
      #   project_error_tracking_setting method which works with Sentry API.
      #
      # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
      #
      if project_error_tracking_setting.integrated_client?
        handle_error_repository_exceptions do
          error = error_repository.find_error(issue_id)
          { issue: error }
        end
      else
        project_error_tracking_setting.issue_details(issue_id: issue_id)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::IssueDetailsService, feature_category: :error_tracking do
  include_context 'sentry error tracking context'

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

  describe '#execute' do
    context 'with authorized user' do
      context 'when issue_details returns a detailed error' do
        let(:detailed_error) { build(:error_tracking_sentry_detailed_error) }
        let(:params) { { issue_id: detailed_error.id } }

        before do
          allow(error_tracking_setting)
            .to receive(:issue_details).and_return(issue: detailed_error)
        end

        it 'returns the detailed error' do
          expect(result).to eq(status: :success, issue: detailed_error)
        end

        it 'returns the gitlab_issue when the error has a sentry_issue' do
          gitlab_issue = create(:issue, project: project)
          create(:sentry_issue, issue: gitlab_issue, sentry_issue_identifier: detailed_error.id)

          expect(result[:issue].gitlab_issue).to include(
            "http", "/#{project.full_path}/-/issues/#{gitlab_issue.iid}"
          )
        end

        it 'returns the gitlab_issue path from sentry when the error has no sentry_issue' do
          expect(result[:issue].gitlab_issue).to eq(detailed_error.gitlab_issue)
        end
      end

      include_examples 'error tracking service data not ready', :issue_details
      include_examples 'error tracking service sentry error handling', :issue_details
      include_examples 'error tracking service http status handling', :issue_details

      context 'with integrated error tracking' do
        let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
        let(:params) { { issue_id: issue_id } }

        before do
          error_tracking_setting.update!(integrated: true)

          allow(service).to receive(:error_repository).and_return(error_repository)
        end

        context 'when error is found' do
          let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
          let(:issue_id) { error.fingerprint }

          before do
            allow(error_repository).to receive(:find_error).with(issue_id).and_return(error)
          end

          it 'returns the error in detailed format' do
            expect(result[:status]).to eq(:success)
            expect(result[:issue]).to eq(error)
          end
        end

        context 'when error does not exist' do
          let(:issue_id) { non_existing_record_id }

          before do
            allow(error_repository).to receive(:find_error).with(issue_id)
              .and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
          end

          it 'returns the error in detailed format' do
            expect(result).to match(
              status: :error,
              message: /Error not found/,
              http_status: :bad_request
            )
          end
        end
      end
    end

    include_examples 'error tracking service unauthorized user'
    include_examples 'error tracking service disabled'
  end
end


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

module ErrorTracking
  class ListProjectsService < ErrorTracking::BaseService
    private

    def perform
      return error('Access denied', :unauthorized) unless can?(current_user, :admin_sentry, project)

      unless project_error_tracking_setting.valid?
        return error(project_error_tracking_setting.errors.full_messages.join(', '), :bad_request)
      end

      response = project_error_tracking_setting.list_sentry_projects

      compose_response(response)
    end

    def parse_response(response)
      { projects: response[:projects] }
    end

    def project_error_tracking_setting
      (super || project.build_error_tracking_setting).tap do |setting|
        setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
          api_host: params[:api_host],
          organization_slug: 'org',
          project_slug: 'proj'
        )

        setting.token = token(setting)
        setting.enabled = true
      end
    end
    strong_memoize_attr :project_error_tracking_setting

    def token(setting)
      return if setting.api_url_changed? && masked_token?

      # Use param token if not masked, otherwise use database token
      return params[:token] unless masked_token?

      setting.token
    end

    def masked_token?
      ErrorTracking::SentryClient::Token.masked_token?(params[:token])
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::ListProjectsService, feature_category: :integrations do
  let_it_be(:user) { create(:user) }
  let_it_be(:project, reload: true) { create(:project) }

  let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/org/proj/' }
  let(:token) { 'test-token' }
  let(:new_api_host) { 'https://gitlab.com/' }
  let(:new_token) { 'new-token' }
  let(:params) { ActionController::Parameters.new(api_host: new_api_host, token: new_token) }

  let(:error_tracking_setting) do
    create(:project_error_tracking_setting, api_url: sentry_url, token: token, project: project)
  end

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

  before do
    project.add_maintainer(user)
  end

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

    context 'with authorized user' do
      before do
        expect(project).to receive(:error_tracking_setting).at_least(:once)
          .and_return(error_tracking_setting)
      end

      context 'set model attributes to new values' do
        let(:new_api_url) { new_api_host + 'api/0/projects/org/proj/' }

        before do
          expect(error_tracking_setting).to receive(:list_sentry_projects)
            .and_return({ projects: [] })
        end

        it 'uses new api_url and token' do
          subject.execute

          expect(error_tracking_setting.api_url).to eq(new_api_url)
          expect(error_tracking_setting.token).to eq(new_token)
          error_tracking_setting.reload
          expect(error_tracking_setting.api_url).to eq(sentry_url)
          expect(error_tracking_setting.token).to eq(token)
        end
      end

      context 'masked param token' do
        let(:params) { ActionController::Parameters.new(token: "*********", api_host: api_host) }

        context 'with the current api host' do
          let(:api_host) { 'https://sentrytest.gitlab.com' }

          before do
            expect(error_tracking_setting).to receive(:list_sentry_projects)
            .and_return({ projects: [] })
          end

          it 'uses database token' do
            expect { subject.execute }.not_to change { error_tracking_setting.token }
          end
        end

        context 'with the similar api host' do
          let(:api_host) { 'https://sentrytest.gitlab.co' }

          it 'returns an error' do
            expect(result[:message]).to start_with('Token is a required field')
            expect(error_tracking_setting).not_to be_valid
            expect(error_tracking_setting).not_to receive(:list_sentry_projects)
          end

          it 'resets the token' do
            expect { subject.execute }.to change { error_tracking_setting.token }.from(token).to(nil)
          end
        end

        context 'with a new api host' do
          let(:api_host) { new_api_host }

          it 'returns an error' do
            expect(result[:message]).to start_with('Token is a required field')
            expect(error_tracking_setting).not_to be_valid
            expect(error_tracking_setting).not_to receive(:list_sentry_projects)
          end

          it 'resets the token' do
            expect { subject.execute }.to change { error_tracking_setting.token }.from(token).to(nil)
          end
        end
      end

      context 'with invalid url' do
        let(:params) do
          ActionController::Parameters.new(
            api_host: 'https://localhost',
            token: new_token
          )
        end

        before do
          error_tracking_setting.enabled = false
        end

        it 'returns error' do
          expect(result[:message]).to start_with('Api url is blocked')
          expect(error_tracking_setting).not_to be_valid
        end
      end

      context 'when list_sentry_projects returns projects' do
        let(:projects) { [:list, :of, :projects] }

        before do
          expect(error_tracking_setting)
            .to receive(:list_sentry_projects).and_return(projects: projects)
        end

        it 'returns the projects' do
          expect(result).to eq(status: :success, projects: projects)
        end
      end
    end

    context 'with unauthorized user' do
      before do
        project.add_guest(user)
      end

      it 'returns error' do
        expect(result).to include(status: :error, message: 'Access denied', http_status: :unauthorized)
      end
    end

    context 'with user with insufficient permissions' do
      before do
        project.add_developer(user)
      end

      it 'returns error' do
        expect(result).to include(status: :error, message: 'Access denied', http_status: :unauthorized)
      end
    end

    context 'with error tracking disabled' do
      before do
        expect(project).to receive(:error_tracking_setting).at_least(:once)
          .and_return(error_tracking_setting)
        expect(error_tracking_setting)
          .to receive(:list_sentry_projects).and_return(projects: [])

        error_tracking_setting.enabled = false
      end

      it 'ignores enabled flag' do
        expect(result).to include(status: :success, projects: [])
      end
    end

    context 'error_tracking_setting is nil' do
      let(:error_tracking_setting) { build(:project_error_tracking_setting, project: project) }
      let(:new_api_url) { new_api_host + 'api/0/projects/org/proj/' }

      before do
        expect(project).to receive(:build_error_tracking_setting).once
          .and_return(error_tracking_setting)

        expect(error_tracking_setting).to receive(:list_sentry_projects)
          .and_return(projects: [:project1, :project2])
      end

      it 'builds a new error_tracking_setting' do
        expect(project.error_tracking_setting).to be_nil

        expect(result[:projects]).to eq([:project1, :project2])

        expect(error_tracking_setting.api_url).to eq(new_api_url)
        expect(error_tracking_setting.token).to eq(new_token)
        expect(error_tracking_setting.enabled).to be true
        expect(error_tracking_setting.persisted?).to be false
        expect(error_tracking_setting.project_id).not_to be_nil

        expect(project.error_tracking_setting).to be_nil
      end
    end
  end
end


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

module ErrorTracking
  class BaseService < ::BaseProjectService
    include Gitlab::Utils::UsageData

    def initialize(project, user = nil, params = {})
      super(project: project, current_user: user, params: params.dup)
    end

    def execute
      return unauthorized if unauthorized

      perform
    end

    private

    def perform
      raise NotImplementedError, "#{self.class} does not implement #{__method__}"
    end

    def compose_response(response, &block)
      errors = parse_errors(response)
      return errors if errors

      yield if block

      track_usage_event(params[:tracking_event], current_user.id) if params[:tracking_event]

      success(parse_response(response))
    end

    def parse_response(response)
      raise NotImplementedError, "#{self.class} does not implement #{__method__}"
    end

    def unauthorized
      return error('Error Tracking is not enabled') unless enabled?
      return error('Access denied', :unauthorized) unless can_read?
    end

    def parse_errors(response)
      return error('Not ready. Try again later', :no_content) unless response
      return error(response[:error], http_status_for(response[:error_type])) if response[:error].present?
    end

    def http_status_for(error_type)
      case error_type
      when ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
        :internal_server_error
      else
        :bad_request
      end
    end

    def project_error_tracking_setting
      project.error_tracking_setting
    end

    def enabled?
      project_error_tracking_setting&.enabled?
    end

    def can_read?
      can?(current_user, :read_sentry_issue, project)
    end

    def can_update?
      can?(current_user, :update_sentry_issue, project)
    end

    def error_repository
      Gitlab::ErrorTracking::ErrorRepository.build(project)
    end

    def handle_error_repository_exceptions
      yield
    rescue Gitlab::ErrorTracking::ErrorRepository::DatabaseError => e
      { error: e.message }
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::BaseService, feature_category: :error_tracking do
  describe '#compose_response' do
    let(:project) { build_stubbed(:project) }
    let(:user) { build_stubbed(:user, id: non_existing_record_id) }
    let(:service) { described_class.new(project, user) }

    it 'returns bad_request error when response has an error key' do
      data = { error: 'Unexpected Error' }

      result = service.send(:compose_response, data)

      expect(result[:status]).to be(:error)
      expect(result[:message]).to be('Unexpected Error')
      expect(result[:http_status]).to be(:bad_request)
    end

    it 'returns server error when response has missing key error_type' do
      data = {
        error: 'Unexpected Error',
        error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
      }

      result = service.send(:compose_response, data)

      expect(result[:status]).to be(:error)
      expect(result[:message]).to be('Unexpected Error')
      expect(result[:http_status]).to be(:internal_server_error)
    end

    it 'returns no content when response is nil' do
      data = nil

      result = service.send(:compose_response, data)

      expect(result[:status]).to be(:error)
      expect(result[:message]).to be('Not ready. Try again later')
      expect(result[:http_status]).to be(:no_content)
    end

    context 'when result has no errors key' do
      let(:data) { { thing: :cat } }

      it 'raises NotImplementedError' do
        expect { service.send(:compose_response, data) }
          .to raise_error(NotImplementedError)
      end

      context 'when parse_response is implemented' do
        before do
          allow(service).to receive(:parse_response) do |response|
            { animal: response[:thing] }
          end
        end

        it 'returns successful response' do
          result = service.send(:compose_response, data)

          expect(result[:animal]).to eq(:cat)
          expect(result[:status]).to eq(:success)
        end

        it 'returns successful response with changes from passed block' do
          result = service.send(:compose_response, data) do
            data[:thing] = :fish
          end

          expect(result[:animal]).to eq(:fish)
          expect(result[:status]).to eq(:success)
        end

        context 'when tracking_event is provided' do
          let(:service) { described_class.new(project, user, tracking_event: :error_tracking_view_list) }

          it_behaves_like 'tracking unique hll events' do
            let(:target_event) { 'error_tracking_view_list' }
            let(:expected_value) { non_existing_record_id }
            let(:request) { service.send(:compose_response, data) }
          end
        end
      end
    end
  end
end


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

module ErrorTracking
  class IssueLatestEventService < ErrorTracking::BaseService
    private

    def perform
      response = find_issue_latest_event(params[:issue_id])

      compose_response(response)
    end

    def parse_response(response)
      { latest_event: response[:latest_event] }
    end

    def find_issue_latest_event(issue_id)
      # There are 2 types of the data source for the error tracking feature:
      #
      # * When integrated error tracking is enabled, we use the application database
      #   to read and save error tracking data.
      #
      # * When integrated error tracking is disabled we call
      #   project_error_tracking_setting method which works with Sentry API.
      #
      # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
      #
      if project_error_tracking_setting.integrated_client?
        handle_error_repository_exceptions do
          event = error_repository.last_event_for(issue_id)

          # We use the same response format as project_error_tracking_setting
          # method below for compatibility with existing code.
          { latest_event: event }
        end
      else
        project_error_tracking_setting.issue_latest_event(issue_id: issue_id)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::IssueLatestEventService, feature_category: :error_tracking do
  include_context 'sentry error tracking context'

  let(:params) { {} }

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

  describe '#execute' do
    context 'with authorized user' do
      context 'when issue_latest_event returns an error event' do
        let(:error_event) { build(:error_tracking_sentry_error_event) }

        before do
          allow(error_tracking_setting)
            .to receive(:issue_latest_event).and_return(latest_event: error_event)
        end

        it 'returns the error event' do
          expect(result).to eq(status: :success, latest_event: error_event)
        end
      end

      include_examples 'error tracking service data not ready', :issue_latest_event
      include_examples 'error tracking service sentry error handling', :issue_latest_event
      include_examples 'error tracking service http status handling', :issue_latest_event

      context 'with integrated error tracking' do
        let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
        let(:params) { { issue_id: issue_id } }

        before do
          error_tracking_setting.update!(integrated: true)

          allow(service).to receive(:error_repository).and_return(error_repository)
        end

        context 'when error is found' do
          let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
          let(:event) { build_stubbed(:error_tracking_open_api_error_event, fingerprint: error.fingerprint) }
          let(:issue_id) { error.fingerprint }

          before do
            allow(error_repository).to receive(:last_event_for).with(issue_id).and_return(event)
          end

          it 'returns the latest event in expected format' do
            expect(result[:status]).to eq(:success)
            expect(result[:latest_event]).to eq(event)
          end
        end

        context 'when error does not exist' do
          let(:issue_id) { non_existing_record_id }

          before do
            allow(error_repository).to receive(:last_event_for).with(issue_id)
              .and_raise(Gitlab::ErrorTracking::ErrorRepository::DatabaseError.new('Error not found'))
          end

          it 'returns the error in detailed format' do
            expect(result).to match(
              status: :error,
              message: /Error not found/,
              http_status: :bad_request
            )
          end
        end
      end
    end

    include_examples 'error tracking service unauthorized user'
    include_examples 'error tracking service disabled'
  end
end


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

module ErrorTracking
  class IssueUpdateService < ErrorTracking::BaseService
    private

    def perform
      update_opts = {
        issue_id: params[:issue_id],
        params: update_params
      }

      response = update_issue(update_opts)

      compose_response(response) do
        project_error_tracking_setting.expire_issues_cache
        response[:closed_issue_iid] = update_related_issue&.iid
      end
    end

    def update_related_issue
      issue = related_issue
      return unless issue

      close_and_create_note(issue)
    end

    def close_and_create_note(issue)
      return unless resolving? && issue.opened?

      processed_issue = close_issue(issue)
      return unless processed_issue.reset.closed?

      create_system_note(processed_issue)
      processed_issue
    end

    def close_issue(issue)
      Issues::CloseService
        .new(container: project, current_user: current_user)
        .execute(issue, system_note: false)
    end

    def create_system_note(issue)
      SystemNoteService.close_after_error_tracking_resolve(issue, project, current_user)
    end

    def related_issue
      SentryIssueFinder
        .new(project, current_user: current_user)
        .execute(params[:issue_id])
        &.issue
    end

    def resolving?
      update_params[:status] == 'resolved'
    end

    def update_params
      params.except(:issue_id)
    end

    def parse_response(response)
      {
        updated: response[:updated].present?,
        closed_issue_iid: response[:closed_issue_iid]
      }
    end

    def unauthorized
      return error('Error Tracking is not enabled') unless enabled?
      return error('Access denied', :unauthorized) unless can_update?
    end

    def update_issue(opts)
      # There are 2 types of the data source for the error tracking feature:
      #
      # * When integrated error tracking is enabled, we use the application database
      #   to read and save error tracking data.
      #
      # * When integrated error tracking is disabled we call
      #   project_error_tracking_setting method which works with Sentry API.
      #
      # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
      #
      if project_error_tracking_setting.integrated_client?
        updated = error_repository.update_error(opts[:issue_id], status: opts[:params][:status])

        # We use the same response format as project_error_tracking_setting
        # method below for compatibility with existing code.
        {
          updated: updated
        }
      else
        project_error_tracking_setting.update_issue(**opts)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::IssueUpdateService, feature_category: :error_tracking do
  include_context 'sentry error tracking context'

  let(:arguments) { { issue_id: non_existing_record_id, status: 'resolved' } }

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

  shared_examples 'does not perform close issue flow' do
    it 'does not call the close issue service' do
      update_service.execute

      expect(issue_close_service).not_to have_received(:execute)
    end

    it 'does not create system note' do
      expect(SystemNoteService).not_to receive(:close_after_error_tracking_resolve)
      update_service.execute
    end
  end

  describe '#execute' do
    context 'with authorized user' do
      context 'when update_issue returns success' do
        let(:update_issue_response) { { updated: true } }

        before do
          allow(error_tracking_setting).to receive(:update_issue).and_return(update_issue_response)
        end

        it 'returns the response' do
          expect(update_service.execute).to eq(update_issue_response.merge(status: :success, closed_issue_iid: nil))
        end

        it 'updates any related issue' do
          expect(update_service).to receive(:update_related_issue)

          update_service.execute
        end

        it 'clears the reactive cache' do
          expect(error_tracking_setting).to receive(:expire_issues_cache)

          result
        end

        context 'with related issue and resolving' do
          let(:issue) { create(:issue, project: project) }
          let(:sentry_issue) { create(:sentry_issue, issue: issue) }
          let(:arguments) { { issue_id: sentry_issue.sentry_issue_identifier, status: 'resolved' } }
          let(:issue_close_service) { instance_double('Issues::CloseService') }

          before do
            allow_next_instance_of(SentryIssueFinder) do |finder|
              allow(finder).to receive(:execute).and_return(sentry_issue)
            end

            allow(Issues::CloseService)
              .to receive(:new)
              .and_return(issue_close_service)

            allow(issue_close_service)
              .to receive(:execute)
              .and_return(issue)
          end

          it 'closes the issue' do
            update_service.execute

            expect(issue_close_service)
              .to have_received(:execute)
              .with(issue, system_note: false)
          end

          context 'when issue gets closed' do
            let(:closed_issue) { create(:issue, :closed, project: project) }

            before do
              allow(issue_close_service)
                .to receive(:execute)
                .with(issue, system_note: false)
                .and_return(closed_issue)
            end

            it 'creates a system note' do
              expect(SystemNoteService).to receive(:close_after_error_tracking_resolve)

              update_service.execute
            end

            it 'returns a response with closed issue' do
              expect(update_service.execute).to eq(status: :success, updated: true, closed_issue_iid: closed_issue.iid)
            end
          end

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

            include_examples 'does not perform close issue flow'
          end

          context 'when status is not resolving' do
            let(:arguments) { { issue_id: sentry_issue.sentry_issue_identifier, status: 'ignored' } }

            include_examples 'does not perform close issue flow'
          end
        end
      end

      include_examples 'error tracking service sentry error handling', :update_issue

      context 'with integrated error tracking' do
        let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
        let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
        let(:issue_id) { error.fingerprint }
        let(:arguments) { { issue_id: issue_id, status: 'resolved' } }

        before do
          error_tracking_setting.update!(integrated: true)

          allow(update_service).to receive(:error_repository).and_return(error_repository)
          allow(error_repository).to receive(:update_error)
            .with(issue_id, status: 'resolved').and_return(updated)
        end

        context 'when update succeeded' do
          let(:updated) { true }

          it 'returns success with updated true' do
            expect(project.error_tracking_setting).to receive(:expire_issues_cache)

            expect(update_service.execute).to eq(
              status: :success,
              updated: true,
              closed_issue_iid: nil
            )
          end
        end

        context 'when update failed' do
          let(:updated) { false }

          it 'returns success with updated false' do
            expect(project.error_tracking_setting).to receive(:expire_issues_cache)

            expect(update_service.execute).to eq(
              status: :success,
              updated: false,
              closed_issue_iid: nil
            )
          end
        end
      end
    end

    include_examples 'error tracking service unauthorized user'
    include_examples 'error tracking service disabled'
  end
end


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

module ErrorTracking
  class ListIssuesService < ErrorTracking::BaseService
    DEFAULT_ISSUE_STATUS = 'unresolved'
    DEFAULT_LIMIT = 20
    DEFAULT_SORT = 'last_seen'

    # Sentry client supports 'muted' and 'assigned' but GitLab does not
    ISSUE_STATUS_VALUES = %w[
      resolved
      unresolved
      ignored
    ].freeze

    def external_url
      project_error_tracking_setting&.sentry_external_url
    end

    private

    def perform
      return invalid_status_error unless valid_status?

      sentry_opts = {
        issue_status: issue_status,
        limit: limit,
        search_term: params[:search_term].presence,
        sort: sort,
        cursor: params[:cursor].presence
      }

      response = list_issues(sentry_opts)

      compose_response(response)
    end

    def parse_response(response)
      response.slice(:issues, :pagination)
    end

    def invalid_status_error
      error('Bad Request: Invalid issue_status', http_status_for(:bad_Request))
    end

    def valid_status?
      ISSUE_STATUS_VALUES.include?(issue_status)
    end

    def issue_status
      params[:issue_status] || DEFAULT_ISSUE_STATUS
    end

    def limit
      params[:limit] || DEFAULT_LIMIT
    end

    def sort
      params[:sort] || DEFAULT_SORT
    end

    def list_issues(opts)
      # There are 2 types of the data source for the error tracking feature:
      #
      # * When integrated error tracking is enabled, we use the application database
      #   to read and save error tracking data.
      #
      # * When integrated error tracking is disabled we call
      #   project_error_tracking_setting method which works with Sentry API.
      #
      # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
      #
      if project_error_tracking_setting.integrated_client?
        # We are going to support more options in the future.
        # For now we implement the bare minimum for rendering the list in UI.
        list_opts = {
          filters: { status: opts[:issue_status] },
          query: opts[:search_term],
          sort: opts[:sort],
          limit: opts[:limit],
          cursor: opts[:cursor]
        }

        errors, pagination = error_repository.list_errors(**list_opts)

        pagination_hash = {}
        pagination_hash[:next] = { cursor: pagination.next } if pagination.next
        pagination_hash[:previous] = { cursor: pagination.prev } if pagination.prev

        # We use the same response format as project_error_tracking_setting
        # method below for compatibility with existing code.
        {
          issues: errors,
          pagination: pagination_hash
        }
      else
        project_error_tracking_setting.list_sentry_issues(**opts)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ErrorTracking::ListIssuesService, feature_category: :error_tracking do
  include_context 'sentry error tracking context'

  let(:params) { {} }

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

  describe '#execute' do
    context 'with Sentry backend' do
      let(:params) { { search_term: 'something', sort: 'last_seen', cursor: 'some-cursor' } }

      let(:list_sentry_issues_args) do
        {
          issue_status: 'unresolved',
          limit: 20,
          search_term: 'something',
          sort: 'last_seen',
          cursor: 'some-cursor'
        }
      end

      context 'with authorized user' do
        let(:issues) { [] }

        described_class::ISSUE_STATUS_VALUES.each do |status|
          it "returns the issues with #{status} issue_status" do
            params[:issue_status] = status
            list_sentry_issues_args[:issue_status] = status
            expect_list_sentry_issues_with(list_sentry_issues_args)

            expect(result).to eq(status: :success, pagination: {}, issues: issues)
          end
        end

        it 'returns the issues with no issue_status' do
          expect_list_sentry_issues_with(list_sentry_issues_args)

          expect(result).to eq(status: :success, pagination: {}, issues: issues)
        end

        it 'returns bad request with invalid issue_status' do
          params[:issue_status] = 'assigned'

          expect(error_tracking_setting).not_to receive(:list_sentry_issues)
          expect(result).to eq(message: "Bad Request: Invalid issue_status", status: :error, http_status: :bad_request)
        end

        include_examples 'error tracking service data not ready', :list_sentry_issues
        include_examples 'error tracking service sentry error handling', :list_sentry_issues
        include_examples 'error tracking service http status handling', :list_sentry_issues
      end

      include_examples 'error tracking service unauthorized user'
      include_examples 'error tracking service disabled'

      def expect_list_sentry_issues_with(list_sentry_issues_args)
        expect(error_tracking_setting)
          .to receive(:list_sentry_issues)
          .with(list_sentry_issues_args)
          .and_return(issues: [], pagination: {})
      end
    end

    context 'with integrated error tracking' do
      let(:error_repository) { instance_double(Gitlab::ErrorTracking::ErrorRepository) }
      let(:errors) { [] }
      let(:pagination) { Gitlab::ErrorTracking::ErrorRepository::Pagination.new(nil, nil) }
      let(:opts) { default_opts }

      let(:default_opts) do
        {
          filters: { status: described_class::DEFAULT_ISSUE_STATUS },
          query: nil,
          sort: described_class::DEFAULT_SORT,
          limit: described_class::DEFAULT_LIMIT,
          cursor: nil
        }
      end

      let(:params) { {} }

      before do
        error_tracking_setting.update!(integrated: true)

        allow(service).to receive(:error_repository).and_return(error_repository)
      end

      context 'when errors are found' do
        let(:error) { build_stubbed(:error_tracking_open_api_error, project_id: project.id) }
        let(:errors) { [error] }

        before do
          allow(error_repository).to receive(:list_errors)
            .with(**opts)
            .and_return([errors, pagination])
        end

        context 'without params' do
          it 'returns the errors without pagination' do
            expect(result[:status]).to eq(:success)
            expect(result[:issues]).to eq(errors)
            expect(result[:pagination]).to eq({})
            expect(error_repository).to have_received(:list_errors).with(**opts)
          end
        end

        context 'with pagination' do
          context 'with next page' do
            before do
              pagination.next = 'next cursor'
            end

            it 'has next cursor' do
              expect(result[:pagination]).to eq(next: { cursor: 'next cursor' })
            end
          end

          context 'with prev page' do
            before do
              pagination.prev = 'prev cursor'
            end

            it 'has prev cursor' do
              expect(result[:pagination]).to eq(previous: { cursor: 'prev cursor' })
            end
          end

          context 'with next and prev page' do
            before do
              pagination.next = 'next cursor'
              pagination.prev = 'prev cursor'
            end

            it 'has both cursors' do
              expect(result[:pagination]).to eq(
                next: { cursor: 'next cursor' },
                previous: { cursor: 'prev cursor' }
              )
            end
          end
        end
      end
    end
  end

  describe '#external_url' do
    it 'calls the project setting sentry_external_url' do
      expect(error_tracking_setting).to receive(:sentry_external_url).and_return(sentry_url)

      expect(subject.external_url).to eql sentry_url
    end
  end
end


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

module WebHooks
  class LogExecutionService
    include ::Gitlab::ExclusiveLeaseHelpers

    LOCK_TTL = 15.seconds.freeze
    LOCK_SLEEP = 0.25.seconds.freeze
    LOCK_RETRY = 65

    attr_reader :hook, :log_data, :response_category

    def initialize(hook:, log_data:, response_category:)
      @hook = hook
      @log_data = log_data.transform_keys(&:to_sym)
      @response_category = response_category
    end

    def execute
      update_hook_failure_state
      log_execution
    end

    private

    def log_execution
      mask_response_headers

      log_data[:request_headers]['X-Gitlab-Token'] = _('[REDACTED]') if hook.token?

      WebHookLog.create!(web_hook: hook, **log_data)
    end

    def mask_response_headers
      return unless hook.url_variables?
      return unless log_data.key?(:response_headers)

      variables_map = hook.url_variables.invert.transform_values { "{#{_1}}" }
      regex = Regexp.union(variables_map.keys)

      log_data[:response_headers].transform_values! do |value|
        regex === value ? value.gsub(regex, variables_map) : value
      end
    end

    # Perform this operation within an `Gitlab::ExclusiveLease` lock to make it
    # safe to be called concurrently from different workers.
    def update_hook_failure_state
      in_lock(lock_name, ttl: LOCK_TTL, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRY) do |retried|
        hook.reset # Reload within the lock so properties are guaranteed to be current.

        case response_category
        when :ok
          hook.enable!
        when :error
          hook.backoff!
        when :failed
          hook.failed!
        end

        hook.update_last_failure
      end
    rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
      raise if raise_lock_error?
    end

    def lock_name
      "web_hooks:update_hook_failure_state:#{hook.id}"
    end

    # Allow an error to be raised after failing to obtain a lease only if the hook
    # is not already in the correct failure state.
    def raise_lock_error?
      hook.reset # Reload so properties are guaranteed to be current.

      hook.executable? != (response_category == :ok)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WebHooks::LogExecutionService, feature_category: :webhooks do
  include ExclusiveLeaseHelpers
  using RSpec::Parameterized::TableSyntax

  describe '#execute' do
    around do |example|
      travel_to(Time.current) { example.run }
    end

    let_it_be_with_reload(:project_hook) { create(:project_hook, :token) }

    let(:response_category) { :ok }
    let(:request_headers) { { 'Header' => 'header value' } }
    let(:data) do
      {
        trigger: 'trigger_name',
        url: 'https://example.com',
        request_headers: request_headers,
        request_data: { 'Request Data' => 'request data value' },
        response_body: 'Response body',
        response_status: '200',
        execution_duration: 1.2,
        internal_error_message: 'error message'
      }
    end

    subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }

    it 'logs the data' do
      expect { service.execute }.to change(::WebHookLog, :count).by(1)

      expect(WebHookLog.recent.first).to have_attributes(data)
    end

    it 'updates the last failure' do
      expect(project_hook).to receive(:update_last_failure)

      service.execute
    end

    context 'obtaining an exclusive lease' do
      let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }

      it 'updates failure state using a lease that ensures fresh state is written' do
        service = described_class.new(hook: project_hook, log_data: data, response_category: :error)
        # Write state somewhere else, so that the hook is out-of-date
        WebHook.find(project_hook.id).update!(recent_failures: 5, disabled_until: 10.minutes.from_now, backoff_count: 1)

        lease = stub_exclusive_lease(lease_key, timeout: described_class::LOCK_TTL)

        expect(lease).to receive(:try_obtain)
        expect(lease).to receive(:cancel)
        expect { service.execute }.to change { WebHook.find(project_hook.id).backoff_count }.to(2)
      end

      context 'when a lease cannot be obtained' do
        where(:response_category, :executable, :needs_updating) do
          :ok     | true  | false
          :ok     | false | true
          :failed | true  | true
          :failed | false | false
          :error  | true  | true
          :error  | false | false
        end

        with_them do
          subject(:service) { described_class.new(hook: project_hook, log_data: data, response_category: response_category) }

          before do
            # stub LOCK_RETRY to be 0 in order for tests to run quicker
            stub_const("#{described_class.name}::LOCK_RETRY", 0)
            stub_exclusive_lease_taken(lease_key, timeout: described_class::LOCK_TTL)
            allow(project_hook).to receive(:executable?).and_return(executable)
          end

          it 'raises an error if the hook needs to be updated' do
            if needs_updating
              expect { service.execute }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
            else
              expect { service.execute }.not_to raise_error
            end
          end
        end
      end
    end

    context 'when response_category is :ok' do
      it 'does not increment the failure count' do
        expect { service.execute }.not_to change(project_hook, :recent_failures)
      end

      it 'does not change the disabled_until attribute' do
        expect { service.execute }.not_to change(project_hook, :disabled_until)
      end

      context 'when the hook had previously failed' do
        before do
          project_hook.update!(recent_failures: 2)
        end

        it 'resets the failure count' do
          expect { service.execute }.to change(project_hook, :recent_failures).to(0)
        end
      end
    end

    context 'when response_category is :failed' do
      let(:response_category) { :failed }

      before do
        data[:response_status] = '400'
      end

      it 'increments the failure count' do
        expect { service.execute }.to change(project_hook, :recent_failures).by(1)
      end

      it 'does not change the disabled_until attribute' do
        expect { service.execute }.not_to change(project_hook, :disabled_until)
      end

      it 'does not allow the failure count to overflow' do
        project_hook.update!(recent_failures: 32767)

        expect { service.execute }.not_to change(project_hook, :recent_failures)
      end
    end

    context 'when response_category is :error' do
      let(:response_category) { :error }

      before do
        data[:response_status] = '500'
      end

      it 'backs off' do
        expect(project_hook).to receive(:backoff!)

        service.execute
      end
    end

    context 'with url_variables' do
      before do
        project_hook.update!(
          url: 'http://example1.test/{foo}-{bar}',
          url_variables: { 'foo' => 'supers3cret', 'bar' => 'token' }
        )
      end

      let(:data) { super().merge(response_headers: { 'X-Token-Id' => 'supers3cret-token', 'X-Request' => 'PUBLIC-token' }) }
      let(:expected_headers) { { 'X-Token-Id' => '{foo}-{bar}', 'X-Request' => 'PUBLIC-{bar}' } }

      it 'logs the data and masks response headers' do
        expect { service.execute }.to change(::WebHookLog, :count).by(1)

        expect(WebHookLog.recent.first.response_headers).to eq(expected_headers)
      end
    end

    context 'with X-Gitlab-Token' do
      let(:request_headers) { { 'X-Gitlab-Token' => project_hook.token } }

      it 'redacts the token' do
        service.execute

        expect(WebHookLog.recent.first.request_headers).to include('X-Gitlab-Token' => '[REDACTED]')
      end
    end
  end
end


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

module WebHooks
  class LogDestroyService
    BATCH_SIZE = 1000

    def initialize(web_hook_id)
      @web_hook_id = web_hook_id
    end

    def execute
      next while WebHookLog.delete_batch_for(@web_hook_id, batch_size: BATCH_SIZE)

      ServiceResponse.success
    rescue StandardError => ex
      ServiceResponse.error(message: ex.message)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WebHooks::LogDestroyService, feature_category: :webhooks do
  subject(:service) { described_class.new(hook.id) }

  describe '#execute' do
    shared_examples 'deletes web hook logs for hook' do
      before do
        create_list(:web_hook_log, 3, web_hook: hook)
        hook.destroy! # The LogDestroyService is expected to be called _after_ hook destruction
      end

      it 'deletes the logs' do
        expect { service.execute }
          .to change(WebHookLog, :count).from(3).to(0)
      end

      context 'when the data-set exceeds the batch size' do
        before do
          stub_const("#{described_class}::BATCH_SIZE", 2)
        end

        it 'deletes the logs' do
          expect { service.execute }
            .to change(WebHookLog, :count).from(3).to(0)
        end
      end

      context 'when it encounters an error' do
        before do
          allow(WebHookLog).to receive(:delete_batch_for).and_raise(StandardError.new('bang'))
        end

        it 'reports the error' do
          expect(service.execute)
            .to be_error
            .and have_attributes(message: 'bang')
        end
      end
    end

    context 'with system hook' do
      let!(:hook) { create(:system_hook, url: "http://example.com") }

      it_behaves_like 'deletes web hook logs for hook'
    end

    context 'with project hook' do
      let!(:hook) { create(:project_hook) }

      it_behaves_like 'deletes web hook logs for hook'
    end
  end
end


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

module WebHooks
  # Destroy a hook, and schedule the logs for deletion.
  class DestroyService
    include Services::ReturnServiceResponses

    attr_accessor :current_user

    DENIED = 'Insufficient permissions'

    def initialize(current_user)
      @current_user = current_user
    end

    def execute(web_hook)
      return error(DENIED, 401) unless authorized?(web_hook)

      hook_id = web_hook.id

      if web_hook.destroy
        WebHooks::LogDestroyWorker.perform_async({ 'hook_id' => hook_id })
        Gitlab::AppLogger.info(log_message(web_hook))

        success({ async: false })
      else
        error("Unable to destroy #{web_hook.model_name.human}", 500)
      end
    end

    private

    def log_message(hook)
      "User #{current_user&.id} scheduled a deletion of logs for hook ID #{hook.id}"
    end

    def authorized?(web_hook)
      Ability.allowed?(current_user, :destroy_web_hook, web_hook)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe WebHooks::DestroyService, feature_category: :webhooks do
  let_it_be(:user) { create(:user) }

  subject { described_class.new(user) }

  describe '#execute' do
    # Testing with a project hook only - for permission tests, see policy specs.
    let!(:hook) { create(:project_hook) }
    let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) }

    context 'when the user does not have permission' do
      it 'is an error' do
        expect(subject.execute(hook))
          .to be_error
          .and have_attributes(message: described_class::DENIED)
      end
    end

    context 'when the user does have permission' do
      before do
        hook.project.add_maintainer(user)
      end

      it 'is successful' do
        expect(subject.execute(hook)).to be_success
      end

      it 'destroys the hook' do
        expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0)
      end

      it 'does not destroy logs' do
        expect { subject.execute(hook) }.not_to change(WebHookLog, :count)
      end

      it 'schedules the destruction of logs' do
        expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id })
        expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/))

        subject.execute(hook)
      end

      context 'when the hook fails to destroy' do
        before do
          allow(hook).to receive(:destroy).and_return(false)
        end

        it 'is not a success' do
          expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async)

          r = subject.execute(hook)

          expect(r).to be_error
          expect(r[:message]).to match %r{Unable to destroy}
        end
      end
    end
  end
end


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

module ContainerRegistry
  module Protection
    class CreateRuleService < BaseService
      ALLOWED_ATTRIBUTES = %i[
        container_path_pattern
        push_protected_up_to_access_level
        delete_protected_up_to_access_level
      ].freeze

      def execute
        unless can?(current_user, :admin_container_image, project)
          error_message = _('Unauthorized to create a container registry protection rule')
          return service_response_error(message: error_message)
        end

        container_registry_protection_rule =
          project.container_registry_protection_rules.create(params.slice(*ALLOWED_ATTRIBUTES))

        unless container_registry_protection_rule.persisted?
          return service_response_error(message: container_registry_protection_rule.errors.full_messages.to_sentence)
        end

        ServiceResponse.success(payload: { container_registry_protection_rule: container_registry_protection_rule })
      rescue StandardError => e
        service_response_error(message: e.message)
      end

      private

      def service_response_error(message:)
        ServiceResponse.error(
          message: message,
          payload: { container_registry_protection_rule: nil }
        )
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe ContainerRegistry::Protection::CreateRuleService, '#execute', feature_category: :container_registry do
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }

  let(:service) { described_class.new(project, current_user, params) }
  let(:params) { attributes_for(:container_registry_protection_rule) }

  subject { service.execute }

  shared_examples 'a successful service response' do
    it { is_expected.to be_success }

    it { is_expected.to have_attributes(errors: be_blank) }

    it do
      is_expected.to have_attributes(
        payload: {
          container_registry_protection_rule:
            be_a(ContainerRegistry::Protection::Rule)
              .and(have_attributes(
                container_path_pattern: params[:container_path_pattern],
                push_protected_up_to_access_level: params[:push_protected_up_to_access_level].to_s,
                delete_protected_up_to_access_level: params[:delete_protected_up_to_access_level].to_s
              ))
        }
      )
    end

    it 'creates a new container registry protection rule in the database' do
      expect { subject }.to change { ContainerRegistry::Protection::Rule.count }.by(1)

      expect(
        ContainerRegistry::Protection::Rule.where(
          project: project,
          container_path_pattern: params[:container_path_pattern],
          push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
        )
      ).to exist
    end
  end

  shared_examples 'an erroneous service response' do
    it { is_expected.to be_error }
    it { is_expected.to have_attributes(errors: be_present, payload: include(container_registry_protection_rule: nil)) }

    it 'does not create a new container registry protection rule in the database' do
      expect { subject }.not_to change { ContainerRegistry::Protection::Rule.count }
    end

    it 'does not create a container registry protection rule with the given params' do
      subject

      expect(
        ContainerRegistry::Protection::Rule.where(
          project: project,
          container_path_pattern: params[:container_path_pattern],
          push_protected_up_to_access_level: params[:push_protected_up_to_access_level]
        )
      ).not_to exist
    end
  end

  it_behaves_like 'a successful service response'

  context 'when fields are invalid' do
    context 'when container_path_pattern is invalid' do
      let(:params) { super().merge(container_path_pattern: '') }

      it_behaves_like 'an erroneous service response'

      it { is_expected.to have_attributes(message: match(/Container path pattern can't be blank/)) }
    end

    context 'when delete_protected_up_to_access_level is invalid' do
      let(:params) { super().merge(delete_protected_up_to_access_level: 1000) }

      it_behaves_like 'an erroneous service response'

      it { is_expected.to have_attributes(message: match(/is not a valid delete_protected_up_to_access_level/)) }
    end

    context 'when push_protected_up_to_access_level is invalid' do
      let(:params) { super().merge(push_protected_up_to_access_level: 1000) }

      it_behaves_like 'an erroneous service response'

      it { is_expected.to have_attributes(message: match(/is not a valid push_protected_up_to_access_level/)) }
    end
  end

  context 'with existing container registry protection rule in the database' do
    let_it_be_with_reload(:existing_container_registry_protection_rule) do
      create(:container_registry_protection_rule, project: project)
    end

    context 'when container registry name pattern is slightly different' do
      let(:params) do
        super().merge(
          # The field `container_path_pattern` is unique; this is why we change the value in a minimum way
          container_path_pattern: "#{existing_container_registry_protection_rule.container_path_pattern}-unique",
          push_protected_up_to_access_level:
            existing_container_registry_protection_rule.push_protected_up_to_access_level
        )
      end

      it_behaves_like 'a successful service response'
    end

    context 'when field `container_path_pattern` is taken' do
      let(:params) do
        super().merge(
          container_path_pattern: existing_container_registry_protection_rule.container_path_pattern,
          push_protected_up_to_access_level: :maintainer
        )
      end

      it_behaves_like 'an erroneous service response'

      it { is_expected.to have_attributes(errors: ['Container path pattern has already been taken']) }

      it { expect { subject }.not_to change { existing_container_registry_protection_rule.updated_at } }
    end
  end

  context 'with disallowed params' do
    let(:params) { super().merge(project_id: 1, unsupported_param: 'unsupported_param_value') }

    it_behaves_like 'a successful service response'
  end

  context 'with forbidden user access level (project developer role)' do
    # Because of the access level hierarchy, we can assume that
    # other access levels below developer role will also not be able to
    # create container registry protection rules.
    let_it_be(:current_user) { create(:user).tap { |u| project.add_developer(u) } }

    it_behaves_like 'an erroneous service response'

    it { is_expected.to have_attributes(message: match(/Unauthorized/)) }
  end
end