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

module Projects
  module ContainerRepository
    module ThirdParty
      class DeleteTagsService
        include BaseServiceUtility

        def initialize(container_repository, tag_names)
          @container_repository = container_repository
          @tag_names = tag_names
        end

        # Replace a tag on the registry with a dummy tag.
        # This is a hack as the registry doesn't support deleting individual
        # tags. This code effectively pushes a dummy image and assigns the tag to it.
        # This way when the tag is deleted only the dummy image is affected.
        # This is used to preserve compatibility with third-party registries that
        # don't support fast delete.
        # See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
        def execute
          return success(deleted: []) if @tag_names.empty?

          # generates the blobs for the dummy image
          dummy_manifest = @container_repository.client.generate_empty_manifest(@container_repository.path)
          return error('could not generate manifest') if dummy_manifest.nil?

          deleted_tags = replace_tag_manifests(dummy_manifest)

          # Deletes the dummy image
          # All created tag digests are the same since they all have the same dummy image.
          # a single delete is sufficient to remove all tags with it
          if deleted_tags.any? && @container_repository.delete_tag(deleted_tags.each_value.first)
            success(deleted: deleted_tags.keys)
          else
            error("could not delete tags: #{@tag_names.join(', ')}".truncate(1000))
          end
        end

        private

        # update the manifests of the tags with the new dummy image
        def replace_tag_manifests(dummy_manifest)
          deleted_tags = @tag_names.map do |name|
            digest = @container_repository.client.put_tag(@container_repository.path, name, dummy_manifest)
            next unless digest

            [name, digest]
          end.compact.to_h

          # make sure the digests are the same (it should always be)
          digests = deleted_tags.values.uniq

          # rubocop: disable CodeReuse/ActiveRecord
          Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new('multiple tag digests')) if digests.many?

          deleted_tags
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService, feature_category: :container_registry do
  include_context 'container repository delete tags service shared context'

  let(:service) { described_class.new(repository, tags) }

  describe '#execute' do
    let(:tags) { %w[A Ba] }

    subject { service.execute }

    context 'with tags to delete' do
      it 'deletes the tags by name' do
        stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')

        tags.each { |tag| stub_put_manifest_request(tag) }

        expect_delete_tags(['sha256:dummy'])

        is_expected.to eq(status: :success, deleted: tags)
      end

      it 'succeeds when tag delete returns 404' do
        stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')

        stub_put_manifest_request('A')
        stub_put_manifest_request('Ba')

        stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
          .to_return(status: 404, body: '', headers: {})

        is_expected.to eq(status: :success, deleted: tags)
      end

      context 'with failures' do
        context 'when the dummy manifest generation fails' do
          before do
            stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3', success: false)
          end

          it { is_expected.to eq(status: :error, message: 'could not generate manifest') }
        end

        context 'when updating tags fails' do
          before do
            stub_upload('sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3')

            stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:4435000728ee66e6a80e55637fc22725c256b61de344a2ecdeaac6bdb36e8bc3")
              .to_return(status: 200, body: '', headers: {})
          end

          context 'all tag updates fail' do
            before do
              stub_put_manifest_request('A', 500, {})
              stub_put_manifest_request('Ba', 500, {})
            end

            it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}") }

            context 'when a large list of tag updates fails' do
              let(:tags) { Array.new(1000) { |i| "tag_#{i}" } }

              before do
                expect(service).to receive(:replace_tag_manifests).and_return({})
              end

              it 'truncates the log message' do
                expect(subject).to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}".truncate(1000))
              end
            end
          end

          context 'a single tag update fails' do
            before do
              stub_put_manifest_request('A')
              stub_put_manifest_request('Ba', 500, {})

              stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/manifests/sha256:dummy")
                .to_return(status: 404, body: '', headers: {})
            end

            it { is_expected.to eq(status: :success, deleted: ['A']) }
          end
        end
      end
    end

    context 'with empty tags' do
      let_it_be(:tags) { [] }

      it 'does not remove anything' do
        expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag_by_digest)

        is_expected.to eq(status: :success, deleted: [])
      end
    end
  end
end


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

module Projects
  module ContainerRepository
    module ThirdParty
      class CleanupTagsService < CleanupTagsBaseService
        def initialize(container_repository:, current_user: nil, params: {})
          super

          @params = params.dup
          @counts = { cached_tags_count: 0 }
        end

        def execute
          tags = container_repository.tags
          @counts[:original_size] = tags.size

          filter_out_latest!(tags)
          filter_by_name!(tags)

          tags = truncate(tags)
          populate_from_cache(tags)

          tags = filter_keep_n(tags)
          tags = filter_by_older_than(tags)

          @counts[:before_delete_size] = tags.size

          delete_tags(tags).merge(@counts).tap do |result|
            result[:deleted_size] = result[:deleted]&.size

            result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size]
          end
        end

        private

        def filter_keep_n(tags)
          tags, tags_to_keep = partition_by_keep_n(tags)

          cache_tags(tags_to_keep)

          tags
        end

        def filter_by_older_than(tags)
          tags, tags_to_keep = partition_by_older_than(tags)

          cache_tags(tags_to_keep)

          tags
        end

        def pushed_at(tag)
          tag.created_at
        end

        def truncate(tags)
          @counts[:before_truncate_size] = tags.size
          @counts[:after_truncate_size] = tags.size

          return tags if max_list_size == 0

          # truncate the list to make sure that after the #filter_keep_n
          # execution, the resulting list will be max_list_size
          truncated_size = max_list_size + keep_n_as_integer

          return tags if tags.size <= truncated_size

          tags = tags.sample(truncated_size)
          @counts[:after_truncate_size] = tags.size
          tags
        end

        def populate_from_cache(tags)
          @counts[:cached_tags_count] = cache.populate(tags) if caching_enabled?
        end

        def cache_tags(tags)
          cache.insert(tags, older_than_in_seconds) if caching_enabled?
        end

        def cache
          strong_memoize(:cache) do
            ::Gitlab::ContainerRepository::Tags::Cache.new(container_repository)
          end
        end

        def caching_enabled?
          result = current_application_settings.container_registry_expiration_policies_caching &&
            container_expiration_policy &&
            older_than.present?
          !!result
        end

        def max_list_size
          current_application_settings.container_registry_cleanup_tags_service_max_list_size.to_i
        end

        def current_application_settings
          ::Gitlab::CurrentSettings.current_application_settings
        end
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ContainerRepository::ThirdParty::CleanupTagsService, :clean_gitlab_redis_cache, feature_category: :container_registry do
  using RSpec::Parameterized::TableSyntax

  include_context 'for a cleanup tags service'

  let_it_be(:user) { create(:user) }
  let_it_be(:project, reload: true) { create(:project, :private) }

  let(:repository) { create(:container_repository, :root, project: project) }
  let(:service) { described_class.new(container_repository: repository, current_user: user, params: params) }
  let(:tags) { %w[latest A Ba Bb C D E] }

  before do
    project.add_maintainer(user) if user

    stub_container_registry_config(enabled: true)

    stub_container_registry_tags(
      repository: repository.path,
      tags: tags
    )

    stub_tag_digest('latest', 'sha256:configA')
    stub_tag_digest('A', 'sha256:configA')
    stub_tag_digest('Ba', 'sha256:configB')
    stub_tag_digest('Bb', 'sha256:configB')
    stub_tag_digest('C', 'sha256:configC')
    stub_tag_digest('D', 'sha256:configD')
    stub_tag_digest('E', nil)

    stub_digest_config('sha256:configA', 1.hour.ago)
    stub_digest_config('sha256:configB', 5.days.ago)
    stub_digest_config('sha256:configC', 1.month.ago)
    stub_digest_config('sha256:configD', nil)
  end

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

    it_behaves_like 'when regex matching everything is specified',
      delete_expectations: [%w[A Ba Bb C D E]],
      service_response_extra: {
        before_truncate_size: 6,
        after_truncate_size: 6,
        before_delete_size: 6,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when regex matching everything is specified and latest is not kept',
      delete_expectations: [%w[A Ba Bb C D E latest]],
      service_response_extra: {
        before_truncate_size: 7,
        after_truncate_size: 7,
        before_delete_size: 7,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when delete regex matching specific tags is used',
      service_response_extra: {
        before_truncate_size: 2,
        after_truncate_size: 2,
        before_delete_size: 2,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when delete regex matching specific tags is used with overriding allow regex',
      service_response_extra: {
        before_truncate_size: 1,
        after_truncate_size: 1,
        before_delete_size: 1,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'with allow regex value',
      delete_expectations: [%w[A C D E]],
      service_response_extra: {
        before_truncate_size: 4,
        after_truncate_size: 4,
        before_delete_size: 4,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when keeping only N tags',
      delete_expectations: [%w[Bb Ba C]],
      service_response_extra: {
        before_truncate_size: 4,
        after_truncate_size: 4,
        before_delete_size: 3,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when not keeping N tags',
      delete_expectations: [%w[A Ba Bb C]],
      service_response_extra: {
        before_truncate_size: 4,
        after_truncate_size: 4,
        before_delete_size: 4,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when removing keeping only 3',
      delete_expectations: [%w[Bb Ba C]],
      service_response_extra: {
        before_truncate_size: 6,
        after_truncate_size: 6,
        before_delete_size: 3,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when removing older than 1 day',
      delete_expectations: [%w[Ba Bb C]],
      service_response_extra: {
        before_truncate_size: 6,
        after_truncate_size: 6,
        before_delete_size: 3,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when combining all parameters',
      delete_expectations: [%w[Bb Ba C]],
      service_response_extra: {
        before_truncate_size: 6,
        after_truncate_size: 6,
        before_delete_size: 3,
        cached_tags_count: 0
      },
      supports_caching: true

    it_behaves_like 'when running a container_expiration_policy',
      delete_expectations: [%w[Bb Ba C]],
      service_response_extra: {
        before_truncate_size: 6,
        after_truncate_size: 6,
        before_delete_size: 3,
        cached_tags_count: 0
      },
      supports_caching: true

    context 'when running a container_expiration_policy with caching' do
      let(:user) { nil }
      let(:params) do
        {
          'name_regex_delete' => '.*',
          'keep_n' => 1,
          'older_than' => '1 day',
          'container_expiration_policy' => true
        }
      end

      it 'expects caching to be used' do
        expect_delete(%w[Bb Ba C], container_expiration_policy: true)
        expect_caching

        subject
      end

      context 'when setting set to false' do
        before do
          stub_application_setting(container_registry_expiration_policies_caching: false)
        end

        it 'does not use caching' do
          expect_delete(%w[Bb Ba C], container_expiration_policy: true)
          expect_no_caching

          subject
        end
      end
    end

    context 'when truncating the tags list' do
      let(:params) do
        {
          'name_regex_delete' => '.*',
          'keep_n' => 1
        }
      end

      shared_examples 'returning the response' do
        |status:, original_size:, before_truncate_size:, after_truncate_size:, before_delete_size:|
        it 'returns the response' do
          expect_no_caching

          result = subject

          service_response = expected_service_response(
            status: status,
            original_size: original_size,
            deleted: nil
          ).merge(
            before_truncate_size: before_truncate_size,
            after_truncate_size: after_truncate_size,
            before_delete_size: before_delete_size,
            cached_tags_count: 0
          )

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

      where(:max_list_size, :delete_tags_service_status, :expected_status, :expected_truncated) do
        10 | :success | :success | false
        10 | :error   | :error   | false
        3  | :success | :error   | true
        3  | :error   | :error   | true
        0  | :success | :success | false
        0  | :error   | :error   | false
      end

      with_them do
        before do
          stub_application_setting(container_registry_cleanup_tags_service_max_list_size: max_list_size)
          allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
            allow(service).to receive(:execute).and_return(status: delete_tags_service_status)
          end
        end

        original_size = 7
        keep_n = 1

        it_behaves_like(
          'returning the response',
          status: params[:expected_status],
          original_size: original_size,
          before_truncate_size: original_size - keep_n,
          after_truncate_size: params[:expected_truncated] ? params[:max_list_size] + keep_n : original_size - keep_n,
          # one tag is filtered out with older_than filter
          before_delete_size: params[:expected_truncated] ? params[:max_list_size] : original_size - keep_n - 1
        )
      end
    end

    context 'with caching', :freeze_time do
      let(:params) do
        {
          'name_regex_delete' => '.*',
          'keep_n' => 1,
          'older_than' => '1 day',
          'container_expiration_policy' => true
        }
      end

      let(:tags_and_created_ats) do
        {
          'A' => 1.hour.ago,
          'Ba' => 5.days.ago,
          'Bb' => 5.days.ago,
          'C' => 1.month.ago,
          'D' => nil,
          'E' => nil
        }
      end

      let(:cacheable_tags) { tags_and_created_ats.reject { |_, value| value.nil? } }

      before do
        expect_delete(%w[Bb Ba C], container_expiration_policy: true)
        # We froze time so we need to set the created_at stubs again
        stub_digest_config('sha256:configA', 1.hour.ago)
        stub_digest_config('sha256:configB', 5.days.ago)
        stub_digest_config('sha256:configC', 1.month.ago)
      end

      it 'caches the created_at values' do
        expect_mget(tags_and_created_ats.keys)
        expect_set(cacheable_tags)

        expect(subject).to include(cached_tags_count: 0)
      end

      context 'with cached values' do
        before do
          ::Gitlab::Redis::Cache.with do |redis|
            redis.set(cache_key('C'), rfc3339(1.month.ago))
          end
        end

        it 'uses them' do
          expect_mget(tags_and_created_ats.keys)

          # because C is already in cache, it should not be cached again
          expect_set(cacheable_tags.except('C'))

          # We will ping the container registry for all tags *except* for C because it's cached
          expect(ContainerRegistry::Blob)
            .to receive(:new).with(repository, { "digest" => "sha256:configA" }).and_call_original
          expect(ContainerRegistry::Blob)
            .to receive(:new).with(repository, { "digest" => "sha256:configB" }).twice.and_call_original
          expect(ContainerRegistry::Blob).not_to receive(:new).with(repository, { "digest" => "sha256:configC" })
          expect(ContainerRegistry::Blob)
            .to receive(:new).with(repository, { "digest" => "sha256:configD" }).and_call_original

          expect(subject).to include(cached_tags_count: 1)
        end
      end

      def expect_mget(keys)
        Gitlab::Redis::Cache.with do |redis|
          parameters = keys.map { |k| cache_key(k) }
          expect(redis).to receive(:mget).with(parameters).and_call_original
        end
      end

      def expect_set(tags)
        selected_tags = tags.map do |tag_name, created_at|
          ex = 1.day.seconds - (Time.zone.now - created_at).seconds
          [tag_name, created_at, ex.to_i] if ex.positive?
        end.compact

        return if selected_tags.count.zero?

        Gitlab::Redis::Cache.with do |redis|
          expect(redis).to receive(:pipelined).and_call_original

          expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
            selected_tags.each do |tag_name, created_at, ex|
              expect(pipeline).to receive(:set).with(cache_key(tag_name), rfc3339(created_at), ex: ex).and_call_original
            end
          end
        end
      end

      def cache_key(tag_name)
        "container_repository:{#{repository.id}}:tag:#{tag_name}:created_at"
      end

      def rfc3339(date_time)
        # DateTime rfc3339 is different ActiveSupport::TimeWithZone rfc3339
        # The caching will use DateTime rfc3339
        DateTime.rfc3339(date_time.rfc3339).rfc3339
      end
    end
  end

  private

  def stub_tag_digest(tag, digest)
    allow(repository.client)
      .to receive(:repository_tag_digest)
      .with(repository.path, tag) { digest }

    allow(repository.client)
      .to receive(:repository_manifest)
      .with(repository.path, tag) do
      { 'config' => { 'digest' => digest } } if digest
    end
  end

  def stub_digest_config(digest, created_at)
    allow(repository.client)
      .to receive(:blob)
      .with(repository.path, digest, nil) do
      { 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at
    end
  end

  def expect_caching
    ::Gitlab::Redis::Cache.with do |redis|
      expect(redis).to receive(:mget).and_call_original
      expect(redis).to receive(:pipelined).and_call_original

      expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
        expect(pipeline).to receive(:set).and_call_original
      end
    end
  end
end


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

module Projects
  module GroupLinks
    class DestroyService < BaseService
      def execute(group_link, skip_authorization: false)
        unless valid_to_destroy?(group_link, skip_authorization)
          return ServiceResponse.error(message: 'Not found', reason: :not_found)
        end

        if group_link.project.private?
          TodosDestroyer::ProjectPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, project.id)
        else
          TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, nil, project.id)
        end

        link = group_link.destroy

        refresh_project_authorizations_asynchronously(link.project)

        # Until we compare the inconsistency rates of the new specialized worker and
        # the old approach, we still run AuthorizedProjectsWorker
        # but with some delay and lower urgency as a safety net.
        link.group.refresh_members_authorized_projects(
          priority: UserProjectAccessChangedService::LOW_PRIORITY
        )

        ServiceResponse.success(payload: { link: link })
      end

      private

      def valid_to_destroy?(group_link, skip_authorization)
        return false unless group_link
        return true if skip_authorization

        current_user.can?(:admin_project_group_link, group_link)
      end

      def refresh_project_authorizations_asynchronously(project)
        AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
      end
    end
  end
end

Projects::GroupLinks::DestroyService.prepend_mod_with('Projects::GroupLinks::DestroyService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::GroupLinks::DestroyService, '#execute', feature_category: :groups_and_projects do
  let_it_be(:user) { create :user }
  let_it_be(:project) { create(:project, :private) }
  let_it_be(:group) { create(:group) }
  let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }

  let(:group_access) { Gitlab::Access::DEVELOPER }
  let!(:group_link) { create(:project_group_link, project: project, group: group, group_access: group_access) }

  subject { described_class.new(project, user) }

  shared_examples_for 'removes group from project' do
    it 'removes group from project' do
      expect { subject.execute(group_link) }.to change { project.reload.project_group_links.count }.from(1).to(0)
    end
  end

  shared_examples_for 'returns not_found' do
    it do
      expect do
        result = subject.execute(group_link)

        expect(result[:status]).to eq(:error)
        expect(result[:reason]).to eq(:not_found)
      end.not_to change { project.reload.project_group_links.count }
    end
  end

  context 'if group_link is blank' do
    let!(:group_link) { nil }

    it_behaves_like 'returns not_found'
  end

  context 'if the user does not have access to destroy the link' do
    it_behaves_like 'returns not_found'
  end

  context 'when the user has proper permissions to remove a group-link from a project' do
    context 'when the user is a MAINTAINER in the project' do
      before do
        project.add_maintainer(user)
      end

      it_behaves_like 'removes group from project'

      context 'project authorizations refresh' do
        it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
          expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
            .to receive(:perform_async).with(group_link.project.id)

          subject.execute(group_link)
        end

        it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
          stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)

          expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
            receive(:bulk_perform_in).with(
              1.hour,
              [[group_user.id]],
              batch_delay: 30.seconds, batch_size: 100
            )
          )

          subject.execute(group_link)
        end

        it 'updates project authorizations of users who had access to the project via the group share', :sidekiq_inline do
          expect { subject.execute(group_link) }.to(
            change { Ability.allowed?(group_user, :read_project, project) }
              .from(true).to(false))
        end
      end

      describe 'todos cleanup' do
        context 'when project is private' do
          it 'triggers todos cleanup' do
            expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
            expect(project.private?).to be true

            subject.execute(group_link)
          end
        end

        context 'when project is public or internal' do
          shared_examples_for 'removes confidential todos' do
            it 'does not trigger todos cleanup' do
              expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
              expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
              expect(project.private?).to be false

              subject.execute(group_link)
            end
          end

          context 'when project is public' do
            let(:project) { create(:project, :public) }

            it_behaves_like 'removes confidential todos'
          end

          context 'when project is internal' do
            let(:project) { create(:project, :public) }

            it_behaves_like 'removes confidential todos'
          end
        end
      end
    end
  end

  context 'when skipping authorization' do
    context 'without providing a user' do
      it 'destroys the link' do
        expect do
          described_class.new(project, nil).execute(group_link, skip_authorization: true)
        end.to change { project.reload.project_group_links.count }.by(-1)
      end
    end
  end
end


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

module Projects
  module GroupLinks
    class UpdateService < BaseService
      def initialize(group_link, user = nil)
        super(group_link.project, user)

        @group_link = group_link
      end

      def execute(group_link_params)
        return ServiceResponse.error(message: 'Not found', reason: :not_found) unless allowed_to_update?

        group_link.update!(group_link_params)

        refresh_authorizations if requires_authorization_refresh?(group_link_params)

        ServiceResponse.success
      end

      private

      attr_reader :group_link

      def allowed_to_update?
        current_user.can?(:admin_project_member, project)
      end

      def refresh_authorizations
        AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)

        # Until we compare the inconsistency rates of the new specialized worker and
        # the old approach, we still run AuthorizedProjectsWorker
        # but with some delay and lower urgency as a safety net.
        group_link.group.refresh_members_authorized_projects(
          priority: UserProjectAccessChangedService::LOW_PRIORITY
        )
      end

      def requires_authorization_refresh?(params)
        params.include?(:group_access)
      end
    end
  end
end

Projects::GroupLinks::UpdateService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::GroupLinks::UpdateService, '#execute', feature_category: :groups_and_projects do
  let_it_be(:user) { create :user }
  let_it_be(:group) { create :group }
  let_it_be(:project) { create :project }
  let_it_be(:group_user) { create(:user).tap { |user| group.add_developer(user) } }

  let(:group_access) { Gitlab::Access::DEVELOPER }

  let!(:link) { create(:project_group_link, project: project, group: group, group_access: group_access) }

  let(:expiry_date) { 1.month.from_now.to_date }
  let(:group_link_params) do
    { group_access: Gitlab::Access::GUEST,
      expires_at: expiry_date }
  end

  subject { described_class.new(link, user).execute(group_link_params) }

  shared_examples_for 'returns not_found' do
    it do
      result = subject

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

  context 'when the user does not have proper permissions to update a project group link' do
    it_behaves_like 'returns not_found'
  end

  context 'when user has proper permissions to update a project group link' do
    context 'when the user is a MAINTAINER in the project' do
      before do
        project.add_maintainer(user)
      end

      it 'updates existing link' do
        expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
        expect(link.expires_at).to be_nil

        subject

        link.reload

        expect(link.group_access).to eq(Gitlab::Access::GUEST)
        expect(link.expires_at).to eq(expiry_date)
      end

      context 'project authorizations update' do
        it 'calls AuthorizedProjectUpdate::ProjectRecalculateWorker to update project authorizations' do
          expect(AuthorizedProjectUpdate::ProjectRecalculateWorker)
            .to receive(:perform_async).with(link.project.id)

          subject
        end

        it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker ' \
           'with a delay to update project authorizations' do
          stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)

          expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
            receive(:bulk_perform_in).with(
              1.hour,
              [[group_user.id]],
              batch_delay: 30.seconds, batch_size: 100
            )
          )

          subject
        end

        it 'updates project authorizations of users who had access to the project via the group share',
          :sidekiq_inline do
          expect { subject }.to(
            change { Ability.allowed?(group_user, :developer_access, project) }
              .from(true).to(false))
        end
      end

      context 'with only param not requiring authorization refresh' do
        let(:group_link_params) { { expires_at: Date.tomorrow } }

        it 'does not perform any project authorizations update using ' \
           '`AuthorizedProjectUpdate::ProjectRecalculateWorker`' do
          expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).not_to receive(:perform_async)

          subject
        end
      end
    end
  end
end


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

module Projects
  module GroupLinks
    class CreateService < BaseService
      include GroupLinkable

      def initialize(project, shared_with_group, user, params)
        @shared_with_group = shared_with_group

        super(project, user, params)
      end

      private

      delegate :root_ancestor, to: :project

      def valid_to_create?
        can?(current_user, :read_namespace_via_membership, shared_with_group) && sharing_allowed?
      end

      def build_link
        @link = project.project_group_links.new(
          group: shared_with_group,
          group_access: params[:link_group_access],
          expires_at: params[:expires_at]
        )
      end

      def setup_authorizations
        AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)

        # AuthorizedProjectsWorker uses an exclusive lease per user but
        # specialized workers might have synchronization issues. Until we
        # compare the inconsistency rates of both approaches, we still run
        # AuthorizedProjectsWorker but with some delay and lower urgency as a
        # safety net.
        shared_with_group.refresh_members_authorized_projects(
          priority: UserProjectAccessChangedService::LOW_PRIORITY
        )
      end
    end
  end
end

Projects::GroupLinks::CreateService.prepend_mod_with('Projects::GroupLinks::CreateService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::GroupLinks::CreateService, '#execute', feature_category: :groups_and_projects do
  let_it_be(:user) { create :user }
  let_it_be(:group) { create :group }
  let_it_be(:project) { create(:project, namespace: create(:namespace, :with_namespace_settings)) }
  let_it_be(:group_user) { create(:user).tap { |user| group.add_guest(user) } }

  let(:opts) do
    {
      link_group_access: Gitlab::Access::DEVELOPER,
      expires_at: nil
    }
  end

  subject { described_class.new(project, group, user, opts) }

  shared_examples_for 'not shareable' do
    it 'does not share and returns an error' do
      expect do
        result = subject.execute

        expect(result[:status]).to eq(:error)
        expect(result[:http_status]).to eq(404)
      end.not_to change { project.project_group_links.count }
    end
  end

  shared_examples_for 'shareable' do
    it 'adds group to project' do
      expect do
        result = subject.execute

        expect(result[:status]).to eq(:success)
      end.to change { project.project_group_links.count }.from(0).to(1)
    end
  end

  context 'when user has proper permissions to share a project with a group' do
    before do
      group.add_guest(user)
    end

    context 'when the user is a MAINTAINER in the project' do
      before do
        project.add_maintainer(user)
      end

      it_behaves_like 'shareable'

      it 'updates authorization', :sidekiq_inline do
        expect { subject.execute }.to(
          change { Ability.allowed?(group_user, :read_project, project) }
            .from(false).to(true))
      end

      context 'with specialized project_authorization workers' do
        let_it_be(:other_user) { create(:user) }

        before do
          group.add_developer(other_user)
        end

        it 'schedules authorization update for users with access to group' do
          stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)

          expect(AuthorizedProjectsWorker).not_to(
            receive(:bulk_perform_async)
          )
          expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to(
            receive(:perform_async)
              .with(project.id)
              .and_call_original
          )
          expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
            receive(:bulk_perform_in).with(
              1.hour,
              array_including([user.id], [other_user.id]),
              batch_delay: 30.seconds, batch_size: 100
            ).and_call_original
          )

          subject.execute
        end
      end

      context 'when sharing outside the hierarchy is disabled' do
        let_it_be(:shared_group_parent) do
          create(:group,
            namespace_settings: create(:namespace_settings, prevent_sharing_groups_outside_hierarchy: true)
          )
        end

        let_it_be(:project, reload: true) { create(:project, group: shared_group_parent) }

        it_behaves_like 'not shareable'

        context 'when group is inside hierarchy' do
          let(:group) { create(:group, :private, parent: shared_group_parent) }

          it_behaves_like 'shareable'
        end
      end
    end
  end

  context 'when user does not have permissions to share the project with a group' do
    it_behaves_like 'not shareable'
  end

  context 'when group is blank' do
    let(:group) { nil }

    it_behaves_like 'not shareable'
  end
end


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

module Projects
  module ImportExport
    class ExportService < BaseService
      def initialize(*args)
        super

        @shared = project.import_export_shared
        @logger = Gitlab::Export::Logger.build
      end

      def execute(after_export_strategy = nil)
        unless project.template_source? || can?(current_user, :admin_project, project)
          raise ::Gitlab::ImportExport::Error.permission_error(current_user, project)
        end

        save_all!
        execute_after_export_action(after_export_strategy)
      ensure
        cleanup
      end

      def exporters
        [
          version_saver, avatar_saver, project_tree_saver, uploads_saver,
          repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver, design_repo_saver
        ]
      end

      protected

      def extra_attributes_for_measurement
        {
          current_user: current_user&.name,
          project_full_path: project&.full_path,
          file_path: shared.export_path
        }
      end

      private

      attr_accessor :shared
      attr_reader :logger

      def execute_after_export_action(after_export_strategy)
        return unless after_export_strategy

        unless after_export_strategy.execute(current_user, project)
          notify_error
        end
      end

      def save_all!
        log_info('Project export started')

        if save_exporters && save_export_archive
          log_info('Project successfully exported')
        else
          notify_error!
        end
      end

      def save_exporters
        exporters.all? do |exporter|
          log_info("#{exporter.class.name} saver started")

          exporter.save
        end
      end

      def save_export_archive
        @export_saver ||= Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
      end

      def version_saver
        @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared)
      end

      def avatar_saver
        @avatar_saver ||= Gitlab::ImportExport::AvatarSaver.new(project: project, shared: shared)
      end

      def project_tree_saver
        @project_tree_saver ||= tree_saver_class.new(
          project: project,
          current_user: current_user,
          shared: shared,
          params: params,
          logger: logger)
      end

      def tree_saver_class
        Gitlab::ImportExport::Project::TreeSaver
      end

      def uploads_saver
        @uploads_saver ||= Gitlab::ImportExport::UploadsSaver.new(project: project, shared: shared)
      end

      def repo_saver
        @repo_saver ||= Gitlab::ImportExport::RepoSaver.new(exportable: project, shared: shared)
      end

      def wiki_repo_saver
        @wiki_repo_saver ||= Gitlab::ImportExport::WikiRepoSaver.new(exportable: project, shared: shared)
      end

      def lfs_saver
        @lfs_saver ||= Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
      end

      def snippets_repo_saver
        @snippets_repo_saver ||= Gitlab::ImportExport::SnippetsRepoSaver.new(
          current_user: current_user,
          project: project,
          shared: shared
        )
      end

      def design_repo_saver
        @design_repo_saver ||= Gitlab::ImportExport::DesignRepoSaver.new(exportable: project, shared: shared)
      end

      def cleanup
        FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
      end

      def notify_error!
        notify_error

        raise Gitlab::ImportExport::Error, shared.errors.to_sentence
      end

      def log_info(message)
        logger.info(
          message: message,
          **log_base_data
        )
      end

      def notify_error
        logger.error(
          message: 'Project export error',
          export_errors: shared.errors.join(', '),
          **log_base_data
        )

        user = current_user
        errors = shared.errors

        project.run_after_commit_or_now do |project|
          NotificationService.new.project_not_exported(project, user, errors)
        end
      end

      def log_base_data
        @log_base_data ||= Gitlab::ImportExport::LogUtil.exportable_to_log_payload(project)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do
  describe '#execute' do
    let_it_be(:user) { create(:user) }
    let_it_be(:group) { create(:group) }
    let_it_be_with_reload(:project) { create(:project, group: group) }

    let(:shared) { project.import_export_shared }
    let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }

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

    before do
      project.add_maintainer(user)
    end

    it 'saves the version' do
      expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the avatar' do
      expect(Gitlab::ImportExport::AvatarSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the models' do
      saver_params = {
        project: project,
        current_user: user,
        shared: shared,
        params: {},
        logger: an_instance_of(Gitlab::Export::Logger)
      }
      expect(Gitlab::ImportExport::Project::TreeSaver).to receive(:new).with(saver_params).and_call_original

      service.execute
    end

    it 'saves the uploads' do
      expect(Gitlab::ImportExport::UploadsSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the repo' do
      # This spec errors when run against the EE codebase as there will be a third repository
      # saved (the EE-specific design repository).
      #
      # Instead, skip this test when run within EE. There is a spec for the EE-specific design repo
      # in the corresponding EE spec.
      skip if Gitlab.ee?

      # once for the normal repo, once for the wiki repo, and once for the design repo
      expect(Gitlab::ImportExport::RepoSaver).to receive(:new).exactly(3).times.and_call_original

      service.execute
    end

    it 'saves the wiki repo' do
      expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the design repo' do
      expect(Gitlab::ImportExport::DesignRepoSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the lfs objects' do
      expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original

      service.execute
    end

    it 'saves the snippets' do
      expect_next_instance_of(Gitlab::ImportExport::SnippetsRepoSaver) do |instance|
        expect(instance).to receive(:save).and_call_original
      end

      service.execute
    end

    context 'when all saver services succeed' do
      before do
        allow(service).to receive(:save_exporters).and_return(true)
      end

      it 'logs a successful message' do
        allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)

        expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with(
          hash_including({ message: 'Project export started', project_id: project.id })
        )

        expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with(
          hash_including({ message: 'Project successfully exported', project_id: project.id })
        )

        service.execute
      end

      it 'saves the project in the file system' do
        expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(true)

        service.execute
      end

      context 'when the upload fails' do
        before do
          expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(false)
        end

        it 'notifies the user of an error' do
          expect(service).to receive(:notify_error).and_call_original

          expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
        end
      end

      it 'calls the after export strategy' do
        allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
        expect(after_export_strategy).to receive(:execute)

        service.execute(after_export_strategy)
      end

      context 'when after export strategy fails' do
        before do
          allow(after_export_strategy).to receive(:execute).and_return(false)
          allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
        end

        after do
          service.execute(after_export_strategy)
        end

        it 'removes the remaining exported data' do
          allow(shared).to receive(:archive_path).and_return('whatever')
          allow(FileUtils).to receive(:rm_rf)

          expect(FileUtils).to receive(:rm_rf).with(shared.archive_path)
        end

        it 'notifies the user' do
          expect_next_instance_of(NotificationService) do |instance|
            expect(instance).to receive(:project_not_exported)
          end
        end

        it 'notifies logger' do
          expect(service.instance_variable_get(:@logger)).to receive(:error).with(
            hash_including({ message: 'Project export error', project_id: project.id })
          )
        end
      end
    end

    context 'when saving services fail' do
      before do
        allow(service).to receive(:save_exporters).and_return(false)
      end

      after do
        expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
      end

      it 'removes the remaining exported data' do
        allow(shared).to receive(:archive_path).and_return('whatever')
        allow(FileUtils).to receive(:rm_rf)

        expect(FileUtils).to receive(:rm_rf).with(shared.archive_path)
      end

      it 'notifies the user' do
        expect_next_instance_of(NotificationService) do |instance|
          expect(instance).to receive(:project_not_exported)
        end
      end

      it 'notifies logger' do
        expect(service.instance_variable_get(:@logger)).to receive(:error)
      end

      it 'does not call the export strategy' do
        expect(service).not_to receive(:execute_after_export_action)
      end
    end

    context 'when one of the savers fail unexpectedly' do
      let(:archive_path) { shared.archive_path }

      before do
        allow(service).to receive_message_chain(:uploads_saver, :save).and_return(false)
      end

      it 'removes the remaining exported data' do
        expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)

        expect(project.import_export_upload).to be_nil
        expect(File.exist?(shared.archive_path)).to eq(false)
      end
    end

    context 'when user does not have admin_project permission' do
      let!(:another_user) { create(:user) }

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

      it 'fails' do
        expected_message =
          "User with ID: %s does not have required permissions for Project: %s with ID: %s" %
          [another_user.id, project.name, project.id]
        expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
      end
    end

    it "avoids N+1 when exporting project members" do
      group.add_owner(user)
      group.add_maintainer(create(:user))
      project.add_maintainer(create(:user))

      # warm up
      service.execute

      control = ActiveRecord::QueryRecorder.new { service.execute }

      group.add_maintainer(create(:user))
      project.add_maintainer(create(:user))

      expect { service.execute }.not_to exceed_query_limit(control)
    end
  end
end


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

module Projects
  module ImportExport
    class ParallelExportService
      def initialize(export_job, current_user, after_export_strategy)
        @export_job = export_job
        @current_user = current_user
        @after_export_strategy = after_export_strategy
        @shared = project.import_export_shared
        @logger = Gitlab::Export::Logger.build
      end

      def execute
        log_info('Parallel project export started')

        if save_exporters && save_export_archive
          log_info('Parallel project export finished successfully')
          execute_after_export_action(after_export_strategy)
        else
          notify_error
        end

      ensure
        cleanup
      end

      private

      attr_reader :export_job, :current_user, :after_export_strategy, :shared, :logger

      delegate :project, to: :export_job

      def execute_after_export_action(after_export_strategy)
        return if after_export_strategy.execute(current_user, project)

        notify_error
      end

      def exporters
        [version_saver, exported_relations_merger]
      end

      def save_exporters
        exporters.all? do |exporter|
          log_info("Parallel project export - #{exporter.class.name} saver started")

          exporter.save
        end
      end

      def save_export_archive
        Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
      end

      def version_saver
        @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared)
      end

      def exported_relations_merger
        @relation_saver ||= Gitlab::ImportExport::Project::ExportedRelationsMerger.new(
          export_job: export_job,
          shared: shared)
      end

      def cleanup
        FileUtils.rm_rf(shared.export_path) if File.exist?(shared.export_path)
        FileUtils.rm_rf(shared.archive_path) if File.exist?(shared.archive_path)
      end

      def log_info(message)
        logger.info(
          message: message,
          **log_base_data
        )
      end

      def notify_error
        logger.error(
          message: 'Parallel project export error',
          export_errors: shared.errors.join(', '),
          export_job_id: export_job.id,
          **log_base_data
        )

        NotificationService.new.project_not_exported(project, current_user, shared.errors)
      end

      def log_base_data
        {
          project_id: project.id,
          project_name: project.name,
          project_path: project.full_path
        }
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ImportExport::ParallelExportService, feature_category: :importers do
  let_it_be(:user) { create(:user) }

  let(:export_job) { create(:project_export_job) }
  let(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
  let(:project) { export_job.project }

  before do
    allow_next_instance_of(Gitlab::ImportExport::Project::ExportedRelationsMerger) do |saver|
      allow(saver).to receive(:save).and_return(true)
    end

    allow_next_instance_of(Gitlab::ImportExport::VersionSaver) do |saver|
      allow(saver).to receive(:save).and_return(true)
    end
  end

  describe '#execute' do
    subject(:service) { described_class.new(export_job, user, after_export_strategy) }

    it 'creates a project export archive file' do
      expect(Gitlab::ImportExport::Saver).to receive(:save)
        .with(exportable: project, shared: project.import_export_shared)

      service.execute
    end

    it 'logs export progress' do
      allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)

      logger = service.instance_variable_get(:@logger)
      messages = [
        'Parallel project export started',
        'Parallel project export - Gitlab::ImportExport::VersionSaver saver started',
        'Parallel project export - Gitlab::ImportExport::Project::ExportedRelationsMerger saver started',
        'Parallel project export finished successfully'
      ]
      messages.each do |message|
        expect(logger).to receive(:info).ordered.with(hash_including(message: message))
      end

      service.execute
    end

    it 'executes after export stragegy on export success' do
      allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)

      expect(after_export_strategy).to receive(:execute)

      service.execute
    end

    it 'ensures files are cleaned up' do
      shared = project.import_export_shared
      FileUtils.mkdir_p(shared.archive_path)
      FileUtils.mkdir_p(shared.export_path)

      allow(Gitlab::ImportExport::Saver).to receive(:save).and_raise(StandardError)

      expect { service.execute }.to raise_error(StandardError)

      expect(File.exist?(shared.export_path)).to eq(false)
      expect(File.exist?(shared.archive_path)).to eq(false)
    end

    context 'when export fails' do
      it 'notifies the error to the user' do
        allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(false)

        allow(project.import_export_shared).to receive(:errors).and_return(['Error'])

        expect_next_instance_of(NotificationService) do |instance|
          expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
        end

        service.execute
      end
    end

    context 'when after export stragegy fails' do
      it 'notifies the error to the user' do
        allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true)
        allow(after_export_strategy).to receive(:execute).and_return(false)
        allow(project.import_export_shared).to receive(:errors).and_return(['Error'])

        expect_next_instance_of(NotificationService) do |instance|
          expect(instance).to receive(:project_not_exported).with(project, user, ['Error'])
        end

        service.execute
      end
    end
  end
end


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

module Projects
  module ImportExport
    class RelationExportService
      include Gitlab::ImportExport::CommandLineUtil

      def initialize(relation_export, jid)
        @relation_export = relation_export
        @jid = jid
        @logger = Gitlab::Export::Logger.build
      end

      def execute
        relation_export.update!(status_event: :start, jid: jid)

        mkdir_p(shared.export_path)
        mkdir_p(shared.archive_path)

        if relation_saver.save
          compress_export_path
          upload_compressed_file
          relation_export.finish!
        else
          fail_export(shared.errors.join(', '))
        end
      rescue StandardError => e
        fail_export(e.message)
      ensure
        FileUtils.remove_entry(shared.export_path) if File.exist?(shared.export_path)
        FileUtils.remove_entry(shared.archive_path) if File.exist?(shared.archive_path)
      end

      private

      attr_reader :relation_export, :jid, :logger

      delegate :relation, :project_export_job, to: :relation_export
      delegate :project, to: :project_export_job

      def shared
        project.import_export_shared
      end

      def relation_saver
        case relation
        when Projects::ImportExport::RelationExport::UPLOADS_RELATION
          Gitlab::ImportExport::UploadsSaver.new(project: project, shared: shared)
        when Projects::ImportExport::RelationExport::REPOSITORY_RELATION
          Gitlab::ImportExport::RepoSaver.new(exportable: project, shared: shared)
        when Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION
          Gitlab::ImportExport::WikiRepoSaver.new(exportable: project, shared: shared)
        when Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION
          Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
        when Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION
          Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: nil)
        when Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION
          Gitlab::ImportExport::DesignRepoSaver.new(exportable: project, shared: shared)
        else
          Gitlab::ImportExport::Project::RelationSaver.new(
            project: project,
            shared: shared,
            relation: relation
          )
        end
      end

      def upload_compressed_file
        upload = relation_export.build_upload
        File.open(archive_file_full_path) { |file| upload.export_file = file }
        upload.save!
      end

      def compress_export_path
        tar_czf(archive: archive_file_full_path, dir: shared.export_path)
      end

      def archive_file_full_path
        @archive_file ||= File.join(shared.archive_path, "#{relation}.tar.gz")
      end

      def fail_export(error_message)
        relation_export.update!(status_event: :fail_op, export_error: error_message.truncate(300))

        logger.error(
          message: 'Project relation export failed',
          export_error: error_message,
          relation: relation_export.relation,
          project_export_job_id: project_export_job.id,
          project_name: project.name,
          project_id: project.id
        )
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::ImportExport::RelationExportService, feature_category: :importers do
  using RSpec::Parameterized::TableSyntax

  subject(:service) { described_class.new(relation_export, 'jid') }

  let_it_be(:project_export_job) { create(:project_export_job) }
  let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
  let_it_be(:archive_path) { "#{Dir.tmpdir}/project_archive_spec" }

  let(:relation_export) { create(:project_relation_export, relation: relation, project_export_job: project_export_job) }

  before do
    stub_uploads_object_storage(ImportExportUploader, enabled: false)

    allow(project_export_job.project.import_export_shared).to receive(:export_path).and_return(export_path)
    allow(project_export_job.project.import_export_shared).to receive(:archive_path).and_return(archive_path)
    allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original
  end

  describe '#execute' do
    let(:relation) { 'labels' }

    it 'removes temporary paths used to export files' do
      expect(FileUtils).to receive(:remove_entry).with(export_path)
      expect(FileUtils).to receive(:remove_entry).with(archive_path)

      service.execute
    end

    context 'when saver fails to export relation' do
      before do
        allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver|
          allow(saver).to receive(:save).and_return(false)
        end
      end

      it 'flags export as failed' do
        service.execute

        expect(relation_export.failed?).to eq(true)
      end

      it 'logs failed message' do
        expect_next_instance_of(Gitlab::Export::Logger) do |logger|
          expect(logger).to receive(:error).with(
            export_error: '',
            message: 'Project relation export failed',
            relation: relation_export.relation,
            project_export_job_id: project_export_job.id,
            project_id: project_export_job.project.id,
            project_name: project_export_job.project.name
          )
        end

        service.execute
      end
    end

    context 'when an exception is raised' do
      before do
        allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver|
          allow(saver).to receive(:save).and_raise('Error!')
        end
      end

      it 'flags export as failed' do
        service.execute

        expect(relation_export.failed?).to eq(true)
        expect(relation_export.export_error).to eq('Error!')
      end

      it 'logs exception error message' do
        expect_next_instance_of(Gitlab::Export::Logger) do |logger|
          expect(logger).to receive(:error).with(
            export_error: 'Error!',
            message: 'Project relation export failed',
            relation: relation_export.relation,
            project_export_job_id: project_export_job.id,
            project_id: project_export_job.project.id,
            project_name: project_export_job.project.name
          )
        end

        service.execute
      end
    end

    describe 'relation name and saver class' do
      where(:relation_name, :saver) do
        Projects::ImportExport::RelationExport::UPLOADS_RELATION | Gitlab::ImportExport::UploadsSaver
        Projects::ImportExport::RelationExport::REPOSITORY_RELATION | Gitlab::ImportExport::RepoSaver
        Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION | Gitlab::ImportExport::WikiRepoSaver
        Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION | Gitlab::ImportExport::LfsSaver
        Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION | Gitlab::ImportExport::SnippetsRepoSaver
        Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION | Gitlab::ImportExport::DesignRepoSaver
        Projects::ImportExport::RelationExport::ROOT_RELATION | Gitlab::ImportExport::Project::RelationSaver
        'labels' | Gitlab::ImportExport::Project::RelationSaver
      end

      with_them do
        let(:relation) { relation_name }

        it 'exports relation using correct saver' do
          expect(saver).to receive(:new).and_call_original

          service.execute
        end

        it 'assigns finished status and relation file' do
          service.execute

          expect(relation_export.finished?).to eq(true)
          expect(relation_export.upload.export_file.filename).to eq("#{relation}.tar.gz")
        end
      end
    end
  end
end


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

module Projects
  module HashedStorage
    class MigrateAttachmentsService < BaseAttachmentService
      extend ::Gitlab::Utils::Override

      # List of paths that can be excluded while evaluation if a target can be discarded
      DISCARDABLE_PATHS = %w[tmp tmp/cache tmp/work].freeze

      def initialize(project:, old_disk_path:, logger: nil)
        super

        @skipped = false
      end

      def execute
        origin = find_old_attachments_path(project)

        project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
        target = FileUploader.absolute_base_dir(project)

        @new_disk_path = project.disk_path

        result = move_folder!(origin, target)

        if result
          project.save!(validate: false)

          yield if block_given?
        end

        result
      end

      override :target_path_discardable?
      # Check if target path has discardable content
      #
      # @param [String] new_path
      # @return [Boolean] whether we can discard the target path or not
      def target_path_discardable?(new_path)
        return false unless File.directory?(new_path)

        found = Dir.glob(File.join(new_path, '**', '**'))

        (found - discardable_paths(new_path)).empty?
      end

      private

      def discardable_paths(new_path)
        DISCARDABLE_PATHS.collect { |path| File.join(new_path, path) }
      end

      def find_old_attachments_path(project)
        origin = FileUploader.absolute_base_dir(project)

        # It's possible that old_disk_path does not match project.disk_path.
        # For example, that happens when we rename a project
        #
        origin.sub(/#{Regexp.escape(project.full_path)}\z/, old_disk_path)
      end
    end
  end
end

Projects::HashedStorage::MigrateAttachmentsService.prepend_mod_with('Projects::HashedStorage::MigrateAttachmentsService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::HashedStorage::MigrateAttachmentsService, feature_category: :groups_and_projects do
  subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }

  let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) }
  let(:legacy_storage) { Storage::LegacyProject.new(project) }
  let(:hashed_storage) { Storage::Hashed.new(project) }

  let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
  let(:file_uploader) { build(:file_uploader, project: project) }
  let(:old_disk_path) { File.join(base_path(legacy_storage), upload.path) }
  let(:new_disk_path) { File.join(base_path(hashed_storage), upload.path) }

  describe '#execute' do
    context 'when succeeds' do
      it 'moves attachments to hashed storage layout' do
        expect(File.file?(old_disk_path)).to be_truthy
        expect(File.file?(new_disk_path)).to be_falsey
        expect(File.exist?(base_path(legacy_storage))).to be_truthy
        expect(File.exist?(base_path(hashed_storage))).to be_falsey
        expect(FileUtils).to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage)).and_call_original

        service.execute

        expect(File.exist?(base_path(hashed_storage))).to be_truthy
        expect(File.exist?(base_path(legacy_storage))).to be_falsey
        expect(File.file?(old_disk_path)).to be_falsey
        expect(File.file?(new_disk_path)).to be_truthy
      end

      it 'returns true' do
        expect(service.execute).to be_truthy
      end

      it 'sets skipped to false' do
        service.execute

        expect(service.skipped?).to be_falsey
      end
    end

    context 'when original folder does not exist anymore' do
      before do
        FileUtils.rm_rf(base_path(legacy_storage))
      end

      it 'skips moving folders and go to next' do
        expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))

        service.execute

        expect(File.exist?(base_path(hashed_storage))).to be_falsey
        expect(File.file?(new_disk_path)).to be_falsey
      end

      it 'returns true' do
        expect(service.execute).to be_truthy
      end

      it 'sets skipped to true' do
        service.execute

        expect(service.skipped?).to be_truthy
      end
    end

    context 'when target folder already exists' do
      before do
        FileUtils.mkdir_p(base_path(hashed_storage))
      end

      it 'succeed when target is empty' do
        expect { service.execute }.not_to raise_error
      end

      it 'succeed when target include only discardable items' do
        Projects::HashedStorage::MigrateAttachmentsService::DISCARDABLE_PATHS.each do |path_fragment|
          discardable_path = File.join(base_path(hashed_storage), path_fragment)
          FileUtils.mkdir_p(discardable_path)
        end

        expect { service.execute }.not_to raise_error
      end

      it 'raises AttachmentCannotMoveError when there are non discardable items on target path' do
        not_discardable_path = File.join(base_path(hashed_storage), 'something')
        FileUtils.mkdir_p(not_discardable_path)

        expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))

        expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentCannotMoveError)
      end
    end

    it 'works even when project validation fails' do
      allow(project).to receive(:valid?) { false }

      expect { service.execute }.to change { project.hashed_storage?(:attachments) }.to(true)
    end
  end

  describe '#old_disk_path' do
    it 'returns old disk_path for project' do
      expect(service.old_disk_path).to eq(project.full_path)
    end
  end

  describe '#new_disk_path' do
    it 'returns new disk_path for project' do
      service.execute

      expect(service.new_disk_path).to eq(project.disk_path)
    end
  end

  describe '#target_path_discardable?' do
    it 'returns true when it include only items on the discardable list' do
      hashed_attachments_path = File.join(base_path(hashed_storage))
      Projects::HashedStorage::MigrateAttachmentsService::DISCARDABLE_PATHS.each do |path_fragment|
        discardable_path = File.join(hashed_attachments_path, path_fragment)
        FileUtils.mkdir_p(discardable_path)
      end

      expect(service.target_path_discardable?(hashed_attachments_path)).to be_truthy
    end
  end

  def base_path(storage)
    File.join(FileUploader.root, storage.disk_path)
  end
end


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

module Projects
  module HashedStorage
    AttachmentMigrationError = Class.new(StandardError)

    AttachmentCannotMoveError = Class.new(StandardError)

    class BaseAttachmentService < BaseService
      # Returns the disk_path value before the execution
      attr_reader :old_disk_path

      # Returns the disk_path value after the execution
      attr_reader :new_disk_path

      # Returns the logger currently in use
      attr_reader :logger

      def initialize(project:, old_disk_path:, logger: nil)
        @project = project
        @old_disk_path = old_disk_path
        @logger = logger || Gitlab::AppLogger
      end

      # Return whether this operation was skipped or not
      #
      # @return [Boolean] true if skipped of false otherwise
      def skipped?
        @skipped
      end

      # Check if target path has discardable content
      #
      # @param [String] new_path
      # @return [Boolean] whether we can discard the target path or not
      def target_path_discardable?(new_path)
        false
      end

      protected

      def move_folder!(old_path, new_path)
        unless File.directory?(old_path)
          logger.info("Skipped attachments move from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
          @skipped = true

          return true
        end

        if File.exist?(new_path)
          if target_path_discardable?(new_path)
            discard_path!(new_path)
          else
            logger.error("Cannot move attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")

            raise AttachmentCannotMoveError, "Target path '#{new_path}' already exists"
          end
        end

        # Create base path folder on the new storage layout
        FileUtils.mkdir_p(File.dirname(new_path))

        FileUtils.mv(old_path, new_path)
        logger.info("Project attachments moved from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")

        true
      end

      # Rename a path adding a suffix in order to prevent data-loss.
      #
      # @param [String] new_path
      def discard_path!(new_path)
        discarded_path = "#{new_path}-#{Time.current.utc.to_i}"

        logger.info("Moving existing empty attachments folder from '#{new_path}' to '#{discarded_path}', (PROJECT_ID=#{project.id})")
        FileUtils.mv(new_path, discarded_path)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::HashedStorage::BaseAttachmentService, feature_category: :groups_and_projects do
  let(:project) { create(:project, :repository, storage_version: 0, skip_disk_validation: true) }

  subject(:service) { described_class.new(project: project, old_disk_path: project.full_path, logger: nil) }

  describe '#old_disk_path' do
    it { is_expected.to respond_to :old_disk_path }
  end

  describe '#new_disk_path' do
    it { is_expected.to respond_to :new_disk_path }
  end

  describe '#skipped?' do
    it { is_expected.to respond_to :skipped? }
  end

  describe '#target_path_discardable?' do
    it 'returns false' do
      expect(subject.target_path_discardable?('something/something')).to be_falsey
    end
  end

  describe '#discard_path!' do
    it 'renames target path adding a timestamp at the end' do
      target_path = Dir.mktmpdir
      expect(Dir.exist?(target_path)).to be_truthy

      freeze_time do
        suffix = Time.current.utc.to_i
        subject.send(:discard_path!, target_path)

        expected_renamed_path = "#{target_path}-#{suffix}"

        expect(Dir.exist?(target_path)).to be_falsey
        expect(Dir.exist?(expected_renamed_path)).to be_truthy
      end
    end
  end

  describe '#move_folder!' do
    context 'when old_path is not a directory' do
      it 'adds information to the logger and returns true' do
        Tempfile.create do |old_path| # rubocop:disable Rails/SaveBang
          new_path = "#{old_path}-new"

          expect(subject.send(:move_folder!, old_path, new_path)).to be_truthy
        end
      end
    end
  end
end


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

module Projects
  module HashedStorage
    class MigrationService < BaseService
      attr_reader :logger, :old_disk_path

      def initialize(project, old_disk_path, logger: nil)
        @project = project
        @old_disk_path = old_disk_path
        @logger = logger || Gitlab::AppLogger
      end

      def execute
        # Migrate attachments from Legacy to Hashed Storage
        unless project.hashed_storage?(:attachments)
          return false unless migrate_attachments_service.execute
        end

        true
      end

      private

      def migrate_attachments_service
        HashedStorage::MigrateAttachmentsService.new(project: project, old_disk_path: old_disk_path, logger: logger)
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::HashedStorage::MigrationService, feature_category: :groups_and_projects do
  let(:project) { create(:project, :empty_repo, :wiki_repo, :legacy_storage) }
  let(:logger) { double }
  let!(:project_attachment) { build(:file_uploader, project: project) }
  let(:project_hashed_path) { Storage::Hashed.new(project).disk_path }
  let(:project_legacy_path) { Storage::LegacyProject.new(project).disk_path }
  let(:wiki_hashed_path) { "#{project_hashed_path}.wiki" }
  let(:wiki_legacy_path) { "#{project_legacy_path}.wiki" }

  subject(:service) { described_class.new(project, project.full_path, logger: logger) }

  describe '#execute' do
    context 'attachments migration' do
      let(:project) { create(:project, :empty_repo, :wiki_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }

      let(:attachments_service) do
        Projects::HashedStorage::MigrateAttachmentsService.new(
          project: project,
          old_disk_path: project.full_path,
          logger: logger
        )
      end

      it 'does not delegate migration if attachments are already migrated' do
        project.storage_version = ::Project::LATEST_STORAGE_VERSION
        expect(Projects::HashedStorage::MigrateAttachmentsService).not_to receive(:new)

        service.execute
      end

      it 'migrates legacy attachments to hashed storage' do
        legacy_attachments_path = FileUploader.absolute_base_dir(project)
        hashed_project = project.dup.tap { |p| p.id = project.id }
        hashed_project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
        hashed_attachments_path = FileUploader.absolute_base_dir(hashed_project)

        expect(logger).to receive(:info).with(/Project attachments moved from '#{legacy_attachments_path}' to '#{hashed_attachments_path}'/)

        expect { service.execute }.to change { project.storage_version }.from(1).to(2)
      end
    end
  end
end


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

module Projects
  module Operations
    class UpdateService < BaseService
      def execute
        Projects::UpdateService
          .new(project, current_user, project_update_params)
          .execute
      end

      private

      def project_update_params
        error_tracking_params
          .merge(alerting_setting_params)
          .merge(prometheus_integration_params)
          .merge(incident_management_setting_params)
      end

      def alerting_setting_params
        return {} unless can?(current_user, :read_prometheus_alerts, project)

        attr = params[:alerting_setting_attributes]
        return {} unless attr

        regenerate_token = attr.delete(:regenerate_token)

        if regenerate_token
          attr[:token] = nil
        else
          attr = attr.except(:token)
        end

        { alerting_setting_attributes: attr }
      end

      def error_tracking_params
        settings = params[:error_tracking_setting_attributes]
        return {} if settings.blank?

        if error_tracking_params_partial_updates?(settings)
          error_tracking_params_for_partial_update(settings)
        else
          error_tracking_params_for_update(settings)
        end
      end

      def error_tracking_params_partial_updates?(settings)
        # Help from @splattael :bow:
        # Make sure we're converting to symbols because
        # * ActionController::Parameters#keys returns a list of strings
        # * in specs we're using hashes with symbols as keys
        update_keys = settings.keys.map(&:to_sym)

        # Integrated error tracking works without Sentry integration,
        # so we don't need to update all those values from error_tracking_params_for_update method.
        # Instead we turn it on/off with partial update together with "enabled" attribute.
        # But since its optional, we exclude it from the condition below.
        update_keys.delete(:integrated)

        update_keys == %i[enabled]
      end

      def error_tracking_params_for_partial_update(settings)
        { error_tracking_setting_attributes: settings }
      end

      def error_tracking_params_for_update(settings)
        api_url = ::ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
          api_host: settings[:api_host],
          project_slug: settings.dig(:project, :slug),
          organization_slug: settings.dig(:project, :organization_slug)
        )

        params = {
          error_tracking_setting_attributes: {
            api_url: api_url,
            enabled: settings[:enabled],
            project_name: settings.dig(:project, :name),
            organization_name: settings.dig(:project, :organization_name),
            sentry_project_id: settings.dig(:project, :sentry_project_id)
          }
        }
        params[:error_tracking_setting_attributes][:token] = settings[:token] unless ::ErrorTracking::SentryClient::Token.masked_token?(settings[:token]) # Don't update token if we receive masked value
        params[:error_tracking_setting_attributes][:integrated] = settings[:integrated] unless settings[:integrated].nil?

        params
      end

      def prometheus_integration_params
        return {} unless attrs = params[:prometheus_integration_attributes]

        integration = project.find_or_initialize_integration(::Integrations::Prometheus.to_param)
        integration.assign_attributes(attrs)
        attrs = integration.to_database_hash

        { prometheus_integration_attributes: attrs }
      end

      def incident_management_setting_params
        attrs = params[:incident_management_setting_attributes]
        return {} unless attrs

        regenerate_token = attrs.delete(:regenerate_token)

        if regenerate_token
          attrs[:pagerduty_token] = nil
        else
          attrs = attrs.except(:pagerduty_token)
        end

        { incident_management_setting_attributes: attrs }
      end
    end
  end
end

Projects::Operations::UpdateService.prepend_mod_with('Projects::Operations::UpdateService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::Operations::UpdateService, feature_category: :groups_and_projects do
  let_it_be_with_refind(:project) { create(:project) }
  let_it_be(:user) { create(:user) }

  let(:result) { subject.execute }

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

  describe '#execute' do
    context 'alerting setting' do
      before_all do
        project.add_maintainer(user)
      end

      shared_examples 'no operation' do
        it 'does nothing' do
          expect(result[:status]).to eq(:success)
          expect(project.reload.alerting_setting).to be_nil
        end
      end

      context 'with valid params' do
        let(:params) { { alerting_setting_attributes: alerting_params } }

        shared_examples 'setting creation' do
          it 'creates a setting' do
            expect(project.alerting_setting).to be_nil

            expect(result[:status]).to eq(:success)
            expect(project.reload.alerting_setting).not_to be_nil
          end
        end

        context 'when regenerate_token is not set' do
          let(:alerting_params) { { token: 'some token' } }

          context 'with an existing setting' do
            let!(:alerting_setting) do
              create(:project_alerting_setting, project: project)
            end

            it 'ignores provided token' do
              expect(result[:status]).to eq(:success)
              expect(project.reload.alerting_setting.token)
                .to eq(alerting_setting.token)
            end
          end

          context 'without an existing setting' do
            it_behaves_like 'setting creation'
          end
        end

        context 'when regenerate_token is set' do
          let(:alerting_params) { { regenerate_token: true } }

          context 'with an existing setting' do
            let(:token) { 'some token' }

            let!(:alerting_setting) do
              create(:project_alerting_setting, project: project, token: token)
            end

            it 'regenerates token' do
              expect(result[:status]).to eq(:success)
              expect(project.reload.alerting_setting.token).not_to eq(token)
            end
          end

          context 'without an existing setting' do
            it_behaves_like 'setting creation'

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

              it_behaves_like 'no operation'
            end
          end
        end
      end

      context 'with empty params' do
        let(:params) { {} }

        it_behaves_like 'no operation'
      end
    end

    context 'error tracking' do
      context 'with existing error tracking setting' do
        let(:params) do
          {
            error_tracking_setting_attributes: {
              enabled: false,
              integrated: true,
              api_host: 'http://gitlab.com/',
              token: 'token',
              project: {
                slug: 'project',
                name: 'Project',
                organization_slug: 'org',
                organization_name: 'Org'
              }
            }
          }
        end

        before do
          create(:project_error_tracking_setting, project: project)
        end

        it 'updates the settings' do
          expect(result[:status]).to eq(:success)

          project.reload
          expect(project.error_tracking_setting).not_to be_enabled
          expect(project.error_tracking_setting.integrated).to be_truthy
          expect(project.error_tracking_setting.api_url).to eq(
            'http://gitlab.com/api/0/projects/org/project/'
          )
          expect(project.error_tracking_setting.token).to eq('token')
          expect(project.error_tracking_setting[:project_name]).to eq('Project')
          expect(project.error_tracking_setting[:organization_name]).to eq('Org')
        end

        context 'disable error tracking' do
          before do
            params[:error_tracking_setting_attributes][:api_host] = ''
            params[:error_tracking_setting_attributes][:enabled] = false
          end

          it 'can set api_url to nil' do
            expect(result[:status]).to eq(:success)

            project.reload
            expect(project.error_tracking_setting).not_to be_enabled
            expect(project.error_tracking_setting.api_url).to be_nil
            expect(project.error_tracking_setting.token).to eq('token')
            expect(project.error_tracking_setting[:project_name]).to eq('Project')
            expect(project.error_tracking_setting[:organization_name]).to eq('Org')
          end
        end
      end

      context 'without an existing error tracking setting' do
        let(:params) do
          {
            error_tracking_setting_attributes: {
              enabled: true,
              integrated: true,
              api_host: 'http://gitlab.com/',
              token: 'token',
              project: {
                slug: 'project',
                name: 'Project',
                organization_slug: 'org',
                organization_name: 'Org'
              }
            }
          }
        end

        it 'creates a setting' do
          expect(result[:status]).to eq(:success)

          expect(project.error_tracking_setting).to be_enabled
          expect(project.error_tracking_setting.integrated).to be_truthy
          expect(project.error_tracking_setting.api_url).to eq(
            'http://gitlab.com/api/0/projects/org/project/'
          )
          expect(project.error_tracking_setting.token).to eq('token')
          expect(project.error_tracking_setting[:project_name]).to eq('Project')
          expect(project.error_tracking_setting[:organization_name]).to eq('Org')
        end
      end

      context 'partial_update' do
        let(:params) do
          {
            error_tracking_setting_attributes: {
              enabled: true
            }
          }
        end

        context 'with setting' do
          before do
            create(:project_error_tracking_setting, :disabled, project: project)
          end

          it 'service succeeds' do
            expect(result[:status]).to eq(:success)
          end

          it 'updates attributes' do
            expect { result }
              .to change { project.reload.error_tracking_setting.enabled }
              .from(false)
              .to(true)
          end

          it 'only updates enabled attribute' do
            result

            expect(project.error_tracking_setting.previous_changes.keys)
              .to contain_exactly('enabled')
          end

          context 'with integrated attribute' do
            let(:params) do
              {
                error_tracking_setting_attributes: {
                  enabled: true,
                  integrated: true
                }
              }
            end

            it 'updates integrated attribute' do
              expect { result }
                .to change { project.reload.error_tracking_setting.integrated }
                .from(false)
                .to(true)
            end

            it 'only updates enabled and integrated attributes' do
              result

              expect(project.error_tracking_setting.previous_changes.keys)
                .to contain_exactly('enabled', 'integrated')
            end
          end
        end

        context 'without setting' do
          it 'creates setting with default values' do
            expect(result[:status]).to eq(:success)
            expect(project.error_tracking_setting.enabled).to be_truthy
            expect(project.error_tracking_setting.integrated).to be_truthy
          end
        end
      end

      context 'with masked param token' do
        let(:params) do
          {
            error_tracking_setting_attributes: {
              api_host: 'https://sentrytest.gitlab.com/',
              project: {
                slug: 'sentry-project',
                organization_slug: 'sentry-org'
              },
              enabled: false,
              token: '*' * 8
            }
          }
        end

        before do
          create(:project_error_tracking_setting, project: project, token: 'token', api_url: 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/')
        end

        it 'does not update token' do
          expect(result[:status]).to eq(:success)

          expect(project.error_tracking_setting.token).to eq('token')
        end
      end

      context 'with invalid parameters' do
        let(:params) { {} }

        let!(:error_tracking_setting) do
          create(:project_error_tracking_setting, project: project)
        end

        it 'does nothing' do
          expect(result[:status]).to eq(:success)
          expect(project.reload.error_tracking_setting)
            .to eq(error_tracking_setting)
        end
      end
    end

    context 'with inappropriate params' do
      let(:params) { { name: '' } }

      let!(:original_name) { project.name }

      it 'ignores params' do
        expect(result[:status]).to eq(:success)
        expect(project.reload.name).to eq(original_name)
      end
    end

    context 'prometheus integration' do
      context 'prometheus params were passed into service' do
        let!(:prometheus_integration) do
          create(:prometheus_integration, :instance, properties: {
            api_url: "http://example.prometheus.com",
            manual_configuration: "0",
            google_iap_audience_client_id: 123
          })
        end

        let(:params) do
          {
            prometheus_integration_attributes: {
              'api_url' => 'http://new.prometheus.com',
              'manual_configuration' => '1'
            }
          }
        end

        it 'uses Project#find_or_initialize_integration to include instance defined defaults and pass them to Projects::UpdateService', :aggregate_failures do
          expect(Projects::UpdateService).to receive(:new) do |project_arg, user_arg, update_params_hash|
            prometheus_attrs = update_params_hash[:prometheus_integration_attributes]

            expect(project_arg).to eq project
            expect(user_arg).to eq user
            expect(prometheus_attrs).to have_key('encrypted_properties')
            expect(prometheus_attrs.keys).not_to include(*%w[id project_id created_at updated_at properties])
            expect(prometheus_attrs['encrypted_properties']).not_to eq(prometheus_integration.encrypted_properties)
          end.and_call_original

          expect { subject.execute }.to change(Integrations::Prometheus, :count).by(1)

          expect(Integrations::Prometheus.last).to have_attributes(
            api_url: 'http://new.prometheus.com',
            manual_configuration: true,
            google_iap_audience_client_id: 123
          )
        end
      end

      context 'when prometheus params are not passed into service' do
        let(:params) { { something: :else } }

        it 'does not pass any prometheus params into Projects::UpdateService', :aggregate_failures do
          project_update_service = double(Projects::UpdateService)

          expect(project).not_to receive(:find_or_initialize_integration)
          expect(Projects::UpdateService)
            .to receive(:new)
            .with(project, user, {})
            .and_return(project_update_service)
          expect(project_update_service).to receive(:execute)

          subject.execute
        end
      end
    end
  end
end


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

module Projects
  module AutoDevops
    class DisableService < BaseService
      def execute
        return false unless implicitly_enabled_and_first_pipeline_failure?

        disable_auto_devops
      end

      private

      def implicitly_enabled_and_first_pipeline_failure?
        project.has_auto_devops_implicitly_enabled? &&
          first_pipeline_failure?
      end

      # We're using `limit` to optimize `auto_devops pipeline` query,
      # since we only care about the first element, and using only `.count`
      # is an expensive operation. See
      # https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/21172#note_99037378
      # for more context.
      # rubocop: disable CodeReuse/ActiveRecord
      def first_pipeline_failure?
        auto_devops_pipelines.success.limit(1).count == 0 &&
          auto_devops_pipelines.failed.limit(1).count.nonzero?
      end
      # rubocop: enable CodeReuse/ActiveRecord

      def disable_auto_devops
        project.auto_devops_attributes = { enabled: false }
        project.save!
      end

      def auto_devops_pipelines
        @auto_devops_pipelines ||= project.ci_pipelines.auto_devops_source
      end
    end
  end
end

```

Response:
# frozen_string_literal: true
require 'spec_helper'

RSpec.describe Projects::AutoDevops::DisableService, '#execute', feature_category: :auto_devops do
  let(:project) { create(:project, :repository, :auto_devops) }
  let(:auto_devops) { project.auto_devops }

  subject { described_class.new(project).execute }

  context 'when Auto DevOps disabled at instance level' do
    before do
      stub_application_setting(auto_devops_enabled: false)
    end

    it { is_expected.to be_falsy }
  end

  context 'when Auto DevOps enabled at instance level' do
    before do
      stub_application_setting(auto_devops_enabled: true)
    end

    context 'when Auto DevOps explicitly enabled on project' do
      before do
        auto_devops.update!(enabled: true)
      end

      it { is_expected.to be_falsy }
    end

    context 'when Auto DevOps explicitly disabled on project' do
      before do
        auto_devops.update!(enabled: false)
      end

      it { is_expected.to be_falsy }
    end

    context 'when Auto DevOps is implicitly enabled' do
      before do
        auto_devops.update!(enabled: nil)
      end

      context 'when is the first pipeline failure' do
        before do
          create(:ci_pipeline, :failed, :auto_devops_source, project: project)
        end

        it 'disables Auto DevOps for project' do
          subject

          expect(auto_devops.enabled).to eq(false)
        end
      end

      context 'when it is not the first pipeline failure' do
        before do
          create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project)
        end

        it 'explicitly disables Auto DevOps for project' do
          subject

          expect(auto_devops.reload.enabled).to eq(false)
        end
      end

      context 'when an Auto DevOps pipeline has succeeded before' do
        before do
          create(:ci_pipeline, :success, :auto_devops_source, project: project)
        end

        it 'does not disable Auto DevOps for project' do
          subject

          expect(auto_devops.reload.enabled).to be_nil
        end
      end
    end

    context 'when project does not have an Auto DevOps record related' do
      let(:project) { create(:project, :repository) }

      before do
        create(:ci_pipeline, :failed, :auto_devops_source, project: project)
      end

      it 'disables Auto DevOps for project' do
        subject
        auto_devops = project.reload.auto_devops

        expect(auto_devops.enabled).to eq(false)
      end

      it 'creates a ProjectAutoDevops record' do
        expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1)
      end
    end
  end
end


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

module Projects
  module DeployTokens
    class DestroyService < BaseService
      include DeployTokenMethods

      def execute
        destroy_deploy_token(@project, params)
      end
    end
  end
end

Projects::DeployTokens::DestroyService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::DeployTokens::DestroyService, feature_category: :continuous_delivery do
  it_behaves_like 'a deploy token deletion service' do
    let_it_be(:entity) { create(:project) }
    let_it_be(:deploy_token_class) { ProjectDeployToken }
    let_it_be(:deploy_token) { create(:deploy_token, projects: [entity]) }
  end
end


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

module Projects
  module DeployTokens
    class CreateService < BaseService
      include DeployTokenMethods

      def execute
        deploy_token = create_deploy_token_for(@project, current_user, params)

        create_deploy_token_payload_for(deploy_token)
      end
    end
  end
end

Projects::DeployTokens::CreateService.prepend_mod

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::DeployTokens::CreateService, feature_category: :continuous_delivery do
  it_behaves_like 'a deploy token creation service' do
    let(:entity) { create(:project) }
    let(:deploy_token_class) { ProjectDeployToken }
  end
end