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

module LooseForeignKeys
  class ProcessDeletedRecordsService
    BATCH_SIZE = 1000

    def initialize(connection:, modification_tracker: LooseForeignKeys::ModificationTracker.new)
      @connection = connection
      @modification_tracker = modification_tracker
    end

    def execute
      raised_error = false
      tracked_tables.cycle do |table|
        records = load_batch_for_table(table)

        if records.empty?
          tracked_tables.delete(table)
          next
        end

        break if modification_tracker.over_limit?

        loose_foreign_key_definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table]

        next if loose_foreign_key_definitions.empty?

        LooseForeignKeys::BatchCleanerService
          .new(
            parent_table: table,
            loose_foreign_key_definitions: loose_foreign_key_definitions,
            deleted_parent_records: records,
            modification_tracker: modification_tracker)
          .execute

        break if modification_tracker.over_limit?
      end

      ::Gitlab::Metrics::LooseForeignKeysSlis.record_apdex(
        success: !modification_tracker.over_limit?,
        db_config_name: db_config_name
      )

      modification_tracker.stats
    rescue StandardError
      raised_error = true
      raise
    ensure
      ::Gitlab::Metrics::LooseForeignKeysSlis.record_error_rate(
        error: raised_error,
        db_config_name: db_config_name
      )
    end

    private

    attr_reader :connection, :modification_tracker

    def db_config_name
      ::Gitlab::Database.db_config_name(connection)
    end

    def load_batch_for_table(table)
      fully_qualified_table_name = "#{current_schema}.#{table}"
      LooseForeignKeys::DeletedRecord.load_batch_for_table(fully_qualified_table_name, BATCH_SIZE)
    end

    def current_schema
      @current_schema = connection.current_schema
    end

    def tracked_tables
      @tracked_tables ||= Gitlab::Database::LooseForeignKeys.definitions_by_table.keys.shuffle
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe LooseForeignKeys::ProcessDeletedRecordsService, feature_category: :database do
  include MigrationsHelpers

  def create_table_structure
    migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)

    migration.create_table :_test_loose_fk_parent_table_1
    migration.create_table :_test_loose_fk_parent_table_2

    migration.create_table :_test_loose_fk_child_table_1_1 do |t|
      t.bigint :parent_id
    end

    migration.create_table :_test_loose_fk_child_table_1_2 do |t|
      t.bigint :parent_id_with_different_column
    end

    migration.create_table :_test_loose_fk_child_table_2_1 do |t|
      t.bigint :parent_id
    end

    migration.track_record_deletions(:_test_loose_fk_parent_table_1)
    migration.track_record_deletions(:_test_loose_fk_parent_table_2)
  end

  let(:all_loose_foreign_key_definitions) do
    {
      '_test_loose_fk_parent_table_1' => [
        ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
          '_test_loose_fk_child_table_1_1',
          '_test_loose_fk_parent_table_1',
          {
            column: 'parent_id',
            on_delete: :async_delete,
            gitlab_schema: :gitlab_main
          }
        ),
        ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
          '_test_loose_fk_child_table_1_2',
          '_test_loose_fk_parent_table_1',
          {
            column: 'parent_id_with_different_column',
            on_delete: :async_nullify,
            gitlab_schema: :gitlab_main
          }
        )
      ],
      '_test_loose_fk_parent_table_2' => [
        ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
          '_test_loose_fk_child_table_2_1',
          '_test_loose_fk_parent_table_2',
          {
            column: 'parent_id',
            on_delete: :async_delete,
            gitlab_schema: :gitlab_main
          }
        )
      ]
    }
  end

  let(:connection) { ::ApplicationRecord.connection }

  let(:loose_fk_parent_table_1) { table(:_test_loose_fk_parent_table_1) }
  let(:loose_fk_parent_table_2) { table(:_test_loose_fk_parent_table_2) }
  let(:loose_fk_child_table_1_1) { table(:_test_loose_fk_child_table_1_1) }
  let(:loose_fk_child_table_1_2) { table(:_test_loose_fk_child_table_1_2) }
  let(:loose_fk_child_table_2_1) { table(:_test_loose_fk_child_table_2_1) }

  before_all do
    create_table_structure
  end

  after(:all) do
    migration = ActiveRecord::Migration.new

    migration.drop_table :_test_loose_fk_parent_table_1
    migration.drop_table :_test_loose_fk_parent_table_2
    migration.drop_table :_test_loose_fk_child_table_1_1
    migration.drop_table :_test_loose_fk_child_table_1_2
    migration.drop_table :_test_loose_fk_child_table_2_1
  end

  before do
    allow(Gitlab::Database::LooseForeignKeys).to receive(:definitions_by_table)
      .and_return(all_loose_foreign_key_definitions)

    parent_record_1 = loose_fk_parent_table_1.create!
    loose_fk_child_table_1_1.create!(parent_id: parent_record_1.id)
    loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_1.id)

    parent_record_2 = loose_fk_parent_table_1.create!
    2.times { loose_fk_child_table_1_1.create!(parent_id: parent_record_2.id) }
    3.times { loose_fk_child_table_1_2.create!(parent_id_with_different_column: parent_record_2.id) }

    parent_record_3 = loose_fk_parent_table_2.create!
    5.times { loose_fk_child_table_2_1.create!(parent_id: parent_record_3.id) }

    loose_fk_parent_table_1.delete_all
    loose_fk_parent_table_2.delete_all
  end

  describe '#execute' do
    def execute
      ::Gitlab::Database::SharedModel.using_connection(connection) do
        described_class.new(connection: connection).execute
      end
    end

    it 'cleans up all rows' do
      execute

      expect(loose_fk_child_table_1_1.count).to eq(0)
      expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
      expect(loose_fk_child_table_2_1.count).to eq(0)
    end

    it 'returns stats for records cleaned up' do
      stats = execute

      expect(stats[:delete_count]).to eq(8)
      expect(stats[:update_count]).to eq(4)
    end

    it 'records the Apdex as success: true' do
      expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
        .with(success: true, db_config_name: 'main')

      execute
    end

    it 'records the error rate as error: false' do
      expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
        .with(error: false, db_config_name: 'main')

      execute
    end

    context 'when the amount of records to clean up exceeds BATCH_SIZE' do
      before do
        stub_const('LooseForeignKeys::CleanupWorker::BATCH_SIZE', 2)
      end

      it 'cleans up everything over multiple batches' do
        expect(LooseForeignKeys::BatchCleanerService).to receive(:new).exactly(:twice).and_call_original

        execute

        expect(loose_fk_child_table_1_1.count).to eq(0)
        expect(loose_fk_child_table_1_2.where(parent_id_with_different_column: nil).count).to eq(4)
        expect(loose_fk_child_table_2_1.count).to eq(0)
      end
    end

    context 'when the amount of records to clean up exceeds the total MAX_DELETES' do
      def count_deletable_rows
        loose_fk_child_table_1_1.count + loose_fk_child_table_2_1.count
      end

      before do
        allow_next_instance_of(LooseForeignKeys::ModificationTracker) do |instance|
          allow(instance).to receive(:max_deletes).and_return(2)
        end
        stub_const('LooseForeignKeys::CleanerService::DELETE_LIMIT', 1)
      end

      it 'cleans up MAX_DELETES and leaves the rest for the next run' do
        expect { execute }.to change { count_deletable_rows }.by(-2)
        expect(count_deletable_rows).to be > 0
      end

      it 'records the Apdex as success: false' do
        expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_apdex)
          .with(success: false, db_config_name: 'main')

        execute
      end
    end

    context 'when cleanup raises an error' do
      before do
        expect_next_instance_of(::LooseForeignKeys::BatchCleanerService) do |service|
          allow(service).to receive(:execute).and_raise("Something broke")
        end
      end

      it 'records the error rate as error: true and does not increment apdex' do
        expect(::Gitlab::Metrics::LooseForeignKeysSlis).to receive(:record_error_rate)
          .with(error: true, db_config_name: 'main')
        expect(::Gitlab::Metrics::LooseForeignKeysSlis).not_to receive(:record_apdex)

        expect { execute }.to raise_error("Something broke")
      end
    end
  end
end


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

module LooseForeignKeys
  # rubocop: disable CodeReuse/ActiveRecord
  class CleanerService
    DELETE_LIMIT = 1000
    UPDATE_LIMIT = 500

    def initialize(loose_foreign_key_definition:, connection:, deleted_parent_records:, with_skip_locked: false)
      @loose_foreign_key_definition = loose_foreign_key_definition
      @connection = connection
      @deleted_parent_records = deleted_parent_records
      @with_skip_locked = with_skip_locked
    end

    def execute
      result = connection.execute(build_query)

      { affected_rows: result.cmd_tuples, table: loose_foreign_key_definition.from_table }
    end

    def async_delete?
      loose_foreign_key_definition.on_delete == :async_delete
    end

    def async_nullify?
      loose_foreign_key_definition.on_delete == :async_nullify
    end

    private

    attr_reader :loose_foreign_key_definition, :connection, :deleted_parent_records, :with_skip_locked

    def build_query
      query = if async_delete?
                delete_query
              elsif async_nullify?
                update_query
              else
                raise "Invalid on_delete argument: #{loose_foreign_key_definition.on_delete}"
              end

      unless query.include?(%{"#{loose_foreign_key_definition.column}" IN (})
        raise("FATAL: foreign key condition is missing from the generated query: #{query}")
      end

      query
    end

    def arel_table
      @arel_table ||= Arel::Table.new(loose_foreign_key_definition.from_table)
    end

    def primary_keys
      @primary_keys ||= connection.primary_keys(loose_foreign_key_definition.from_table).map { |key| arel_table[key] }
    end

    def quoted_table_name
      @quoted_table_name ||= Arel.sql(connection.quote_table_name(loose_foreign_key_definition.from_table))
    end

    def delete_query
      query = Arel::DeleteManager.new
      query.from(quoted_table_name)

      add_in_query_with_limit(query, DELETE_LIMIT)
    end

    def update_query
      query = Arel::UpdateManager.new
      query.table(quoted_table_name)
      query.set([[arel_table[loose_foreign_key_definition.column], nil]])

      add_in_query_with_limit(query, UPDATE_LIMIT)
    end

    # IN query with one or composite primary key
    # WHERE (primary_key1, primary_key2) IN (subselect)
    def add_in_query_with_limit(query, limit)
      columns = Arel::Nodes::Grouping.new(primary_keys)
      query.where(columns.in(in_query_with_limit(limit))).to_sql
    end

    # Builds the following sub-query
    # SELECT primary_keys FROM table WHERE foreign_key IN (1, 2, 3) LIMIT N
    def in_query_with_limit(limit)
      in_query = Arel::SelectManager.new
      in_query.from(quoted_table_name)
      in_query.where(arel_table[loose_foreign_key_definition.column].in(deleted_parent_records.map(&:primary_key_value)))
      in_query.projections = primary_keys
      in_query.take(limit)
      in_query.lock(Arel.sql('FOR UPDATE SKIP LOCKED')) if with_skip_locked
      in_query
    end
  end
  # rubocop: enable CodeReuse/ActiveRecord
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe LooseForeignKeys::CleanerService, feature_category: :database do
  let(:schema) { ApplicationRecord.connection.current_schema }
  let(:deleted_records) do
    [
      LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: non_existing_record_id),
      LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: non_existing_record_id)
    ]
  end

  let(:loose_fk_definition) do
    ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
      'issues',
      'projects',
      {
        column: 'project_id',
        on_delete: :async_nullify,
        gitlab_schema: :gitlab_main
      }
    )
  end

  subject(:cleaner_service) do
    described_class.new(
      loose_foreign_key_definition: loose_fk_definition,
      connection: ApplicationRecord.connection,
      deleted_parent_records: deleted_records)
  end

  context 'when invalid foreign key definition is passed' do
    context 'when invalid on_delete argument was given' do
      before do
        loose_fk_definition.options[:on_delete] = :invalid
      end

      it 'raises KeyError' do
        expect { cleaner_service.execute }.to raise_error(StandardError, /Invalid on_delete argument/)
      end
    end
  end

  describe 'query generation' do
    context 'when single primary key is used' do
      let(:issue) { create(:issue) }

      let(:deleted_records) do
        [
          LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.projects", primary_key_value: issue.project_id)
        ]
      end

      it 'generates an IN query for nullifying the rows' do
        expected_query = %{UPDATE "issues" SET "project_id" = NULL WHERE ("issues"."id") IN (SELECT "issues"."id" FROM "issues" WHERE "issues"."project_id" IN (#{issue.project_id}) LIMIT 500)}
        expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original

        cleaner_service.execute

        issue.reload
        expect(issue.project_id).to be_nil
      end

      it 'generates an IN query for deleting the rows' do
        loose_fk_definition.options[:on_delete] = :async_delete

        expected_query = %{DELETE FROM "issues" WHERE ("issues"."id") IN (SELECT "issues"."id" FROM "issues" WHERE "issues"."project_id" IN (#{issue.project_id}) LIMIT 1000)}
        expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original

        cleaner_service.execute

        expect(Issue.exists?(id: issue.id)).to eq(false)
      end
    end

    context 'when composite primary key is used' do
      let!(:user) { create(:user) }
      let!(:project) { create(:project) }

      let(:loose_fk_definition) do
        ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
          'project_authorizations',
          'users',
          {
            column: 'user_id',
            on_delete: :async_delete,
            gitlab_schema: :gitlab_main
          }
        )
      end

      let(:deleted_records) do
        [
          LooseForeignKeys::DeletedRecord.new(fully_qualified_table_name: "#{schema}.users", primary_key_value: user.id)
        ]
      end

      subject(:cleaner_service) do
        described_class.new(
          loose_foreign_key_definition: loose_fk_definition,
          connection: ApplicationRecord.connection,
          deleted_parent_records: deleted_records
        )
      end

      before do
        project.add_developer(user)
      end

      it 'generates an IN query for deleting the rows' do
        expected_query = %{DELETE FROM "project_authorizations" WHERE ("project_authorizations"."user_id", "project_authorizations"."project_id", "project_authorizations"."access_level") IN (SELECT "project_authorizations"."user_id", "project_authorizations"."project_id", "project_authorizations"."access_level" FROM "project_authorizations" WHERE "project_authorizations"."user_id" IN (#{user.id}) LIMIT 1000)}
        expect(ApplicationRecord.connection).to receive(:execute).with(expected_query).and_call_original

        cleaner_service.execute

        expect(ProjectAuthorization.exists?(user_id: user.id)).to eq(false)
      end

      context 'when the query generation is incorrect (paranoid check)' do
        it 'raises error if the foreign key condition is missing' do
          expect_next_instance_of(LooseForeignKeys::CleanerService) do |instance|
            expect(instance).to receive(:delete_query).and_return('wrong query')
          end

          expect { cleaner_service.execute }.to raise_error /FATAL: foreign key condition is missing from the generated query/
        end
      end
    end

    context 'when with_skip_locked parameter is true' do
      subject(:cleaner_service) do
        described_class.new(
          loose_foreign_key_definition: loose_fk_definition,
          connection: ApplicationRecord.connection,
          deleted_parent_records: deleted_records,
          with_skip_locked: true
        )
      end

      it 'generates a query with the SKIP LOCKED clause' do
        expect(ApplicationRecord.connection).to receive(:execute).with(/FOR UPDATE SKIP LOCKED/).and_call_original

        cleaner_service.execute
      end
    end
  end
end


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

module LooseForeignKeys
  class BatchCleanerService
    CLEANUP_ATTEMPTS_BEFORE_RESCHEDULE = 3
    CONSUME_AFTER_RESCHEDULE = 5.minutes

    def initialize(parent_table:, loose_foreign_key_definitions:, deleted_parent_records:, modification_tracker: LooseForeignKeys::ModificationTracker.new)
      @parent_table = parent_table
      @loose_foreign_key_definitions = loose_foreign_key_definitions
      @deleted_parent_records = deleted_parent_records
      @modification_tracker = modification_tracker
      @deleted_records_counter = Gitlab::Metrics.counter(
        :loose_foreign_key_processed_deleted_records,
        'The number of processed loose foreign key deleted records'
      )
      @deleted_records_rescheduled_count = Gitlab::Metrics.counter(
        :loose_foreign_key_rescheduled_deleted_records,
        'The number of rescheduled loose foreign key deleted records'
      )
      @deleted_records_incremented_count = Gitlab::Metrics.counter(
        :loose_foreign_key_incremented_deleted_records,
        'The number of loose foreign key deleted records with incremented cleanup_attempts'
      )
    end

    def execute
      loose_foreign_key_definitions.each do |loose_foreign_key_definition|
        run_cleaner_service(loose_foreign_key_definition, with_skip_locked: true)

        if modification_tracker.over_limit?
          handle_over_limit
          break
        end

        run_cleaner_service(loose_foreign_key_definition, with_skip_locked: false)

        if modification_tracker.over_limit?
          handle_over_limit
          break
        end
      end

      return if modification_tracker.over_limit?

      # At this point, all associations are cleaned up, we can update the status of the parent records
      update_count = LooseForeignKeys::DeletedRecord.mark_records_processed(deleted_parent_records)

      deleted_records_counter.increment({ table: parent_table, db_config_name: db_config_name }, update_count)
    end

    private

    attr_reader :parent_table, :loose_foreign_key_definitions, :deleted_parent_records, :modification_tracker, :deleted_records_counter, :deleted_records_rescheduled_count, :deleted_records_incremented_count

    def handle_over_limit
      records_to_reschedule = []
      records_to_increment = []

      deleted_parent_records.each do |deleted_record|
        if deleted_record.cleanup_attempts >= CLEANUP_ATTEMPTS_BEFORE_RESCHEDULE
          records_to_reschedule << deleted_record
        else
          records_to_increment << deleted_record
        end
      end

      reschedule_count = LooseForeignKeys::DeletedRecord.reschedule(records_to_reschedule, CONSUME_AFTER_RESCHEDULE.from_now)
      deleted_records_rescheduled_count.increment({ table: parent_table, db_config_name: db_config_name }, reschedule_count)

      increment_count = LooseForeignKeys::DeletedRecord.increment_attempts(records_to_increment)
      deleted_records_incremented_count.increment({ table: parent_table, db_config_name: db_config_name }, increment_count)
    end

    def record_result(cleaner, result)
      if cleaner.async_delete?
        modification_tracker.add_deletions(result[:table], result[:affected_rows])
      elsif cleaner.async_nullify?
        modification_tracker.add_updates(result[:table], result[:affected_rows])
      end
    end

    def run_cleaner_service(loose_foreign_key_definition, with_skip_locked:)
      base_models_for_gitlab_schema = Gitlab::Database.schemas_to_base_models.fetch(loose_foreign_key_definition.options[:gitlab_schema])
      base_models_for_gitlab_schema.each do |base_model|
        cleaner = CleanerService.new(
          loose_foreign_key_definition: loose_foreign_key_definition,
          connection: base_model.connection,
          deleted_parent_records: deleted_parent_records,
          with_skip_locked: with_skip_locked
        )

        loop do
          result = cleaner.execute
          record_result(cleaner, result)

          break if modification_tracker.over_limit? || result[:affected_rows] == 0
        end
      end
    end

    def db_config_name
      LooseForeignKeys::DeletedRecord.connection.pool.db_config.name
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe LooseForeignKeys::BatchCleanerService, feature_category: :database do
  include MigrationsHelpers

  def create_table_structure
    migration = ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers)

    migration.create_table :_test_loose_fk_parent_table

    migration.create_table :_test_loose_fk_child_table_1 do |t|
      t.bigint :parent_id
    end

    migration.create_table :_test_loose_fk_child_table_2 do |t|
      t.bigint :parent_id_with_different_column
    end

    migration.track_record_deletions(:_test_loose_fk_parent_table)
  end

  let(:loose_foreign_key_definitions) do
    [
      ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
        '_test_loose_fk_child_table_1',
        '_test_loose_fk_parent_table',
        {
          column: 'parent_id',
          on_delete: :async_delete,
          gitlab_schema: :gitlab_main
        }
      ),
      ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
        '_test_loose_fk_child_table_2',
        '_test_loose_fk_parent_table',
        {
          column: 'parent_id_with_different_column',
          on_delete: :async_nullify,
          gitlab_schema: :gitlab_main
        }
      )
    ]
  end

  let(:loose_fk_parent_table) { table(:_test_loose_fk_parent_table) }
  let(:loose_fk_child_table_1) { table(:_test_loose_fk_child_table_1) }
  let(:loose_fk_child_table_2) { table(:_test_loose_fk_child_table_2) }
  let(:parent_record_1) { loose_fk_parent_table.create! }
  let(:other_parent_record) { loose_fk_parent_table.create! }

  before_all do
    create_table_structure
  end

  before do
    parent_record_1

    loose_fk_child_table_1.create!(parent_id: parent_record_1.id)
    loose_fk_child_table_1.create!(parent_id: parent_record_1.id)

    # these will not be deleted
    loose_fk_child_table_1.create!(parent_id: other_parent_record.id)
    loose_fk_child_table_1.create!(parent_id: other_parent_record.id)

    loose_fk_child_table_2.create!(parent_id_with_different_column: parent_record_1.id)
    loose_fk_child_table_2.create!(parent_id_with_different_column: parent_record_1.id)

    # these will not be deleted
    loose_fk_child_table_2.create!(parent_id_with_different_column: other_parent_record.id)
    loose_fk_child_table_2.create!(parent_id_with_different_column: other_parent_record.id)
  end

  after(:all) do
    migration = ActiveRecord::Migration.new
    migration.drop_table :_test_loose_fk_parent_table
    migration.drop_table :_test_loose_fk_child_table_1
    migration.drop_table :_test_loose_fk_child_table_2
  end

  context 'when parent records are deleted' do
    let(:deleted_records_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_processed_deleted_records) }

    before do
      parent_record_1.delete

      expect(loose_fk_child_table_1.count).to eq(4)
      expect(loose_fk_child_table_2.count).to eq(4)

      described_class.new(
        parent_table: '_test_loose_fk_parent_table',
        loose_foreign_key_definitions: loose_foreign_key_definitions,
        deleted_parent_records: LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 100)
      ).execute
    end

    it 'cleans up the child records' do
      expect(loose_fk_child_table_1.where(parent_id: parent_record_1.id)).to be_empty
      expect(loose_fk_child_table_2.where(parent_id_with_different_column: nil).count).to eq(2)
    end

    it 'cleans up the pending parent DeletedRecord' do
      expect(LooseForeignKeys::DeletedRecord.status_pending.count).to eq(0)
      expect(LooseForeignKeys::DeletedRecord.status_processed.count).to eq(1)
    end

    it 'records the DeletedRecord status updates', :prometheus do
      counter = Gitlab::Metrics.registry.get(:loose_foreign_key_processed_deleted_records)

      expect(counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
    end

    it 'does not delete unrelated records' do
      expect(loose_fk_child_table_1.where(parent_id: other_parent_record.id).count).to eq(2)
      expect(loose_fk_child_table_2.where(parent_id_with_different_column: other_parent_record.id).count).to eq(2)
    end
  end

  describe 'fair queueing' do
    context 'when the execution is over the limit' do
      let(:modification_tracker) { instance_double(LooseForeignKeys::ModificationTracker) }
      let(:over_limit_return_values) { [true] }
      let(:deleted_record) { LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 1).first }
      let(:deleted_records_rescheduled_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_rescheduled_deleted_records) }
      let(:deleted_records_incremented_counter) { Gitlab::Metrics.registry.get(:loose_foreign_key_incremented_deleted_records) }

      let(:cleaner) do
        described_class.new(
          parent_table: '_test_loose_fk_parent_table',
          loose_foreign_key_definitions: loose_foreign_key_definitions,
          deleted_parent_records: LooseForeignKeys::DeletedRecord.load_batch_for_table('public._test_loose_fk_parent_table', 100),
          modification_tracker: modification_tracker
        )
      end

      before do
        parent_record_1.delete
        allow(modification_tracker).to receive(:over_limit?).and_return(*over_limit_return_values)
        allow(modification_tracker).to receive(:add_deletions)
      end

      context 'when the deleted record is under the maximum allowed cleanup attempts' do
        it 'updates the cleanup_attempts column', :aggregate_failures do
          deleted_record.update!(cleanup_attempts: 1)

          cleaner.execute

          expect(deleted_record.reload.cleanup_attempts).to eq(2)
          expect(deleted_records_incremented_counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
        end

        context 'when the deleted record is above the maximum allowed cleanup attempts' do
          it 'reschedules the record', :aggregate_failures do
            deleted_record.update!(cleanup_attempts: LooseForeignKeys::BatchCleanerService::CLEANUP_ATTEMPTS_BEFORE_RESCHEDULE + 1)

            freeze_time do
              cleaner.execute

              expect(deleted_record.reload).to have_attributes(
                cleanup_attempts: 0,
                consume_after: 5.minutes.from_now
              )
              expect(deleted_records_rescheduled_counter.get(table: loose_fk_parent_table.table_name, db_config_name: 'main')).to eq(1)
            end
          end
        end

        describe 'when over limit happens on the second cleanup call without skip locked' do
          # over_limit? is called twice, we test here the 2nd call
          # - When invoking cleanup with SKIP LOCKED
          # - When invoking cleanup (no SKIP LOCKED)
          let(:over_limit_return_values) { [false, true] }

          it 'updates the cleanup_attempts column' do
            expect(cleaner).to receive(:run_cleaner_service).twice

            deleted_record.update!(cleanup_attempts: 1)

            cleaner.execute

            expect(deleted_record.reload.cleanup_attempts).to eq(2)
          end
        end
      end
    end
  end
end


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

#
# Used by NotificationService to determine who should receive notification
#
module NotificationRecipients
  module BuildService
    def self.notifiable_users(users, *args)
      users.compact.map { |u| NotificationRecipient.new(u, *args) }.select(&:notifiable?).map(&:user)
    end

    def self.notifiable?(user, *args)
      NotificationRecipient.new(user, *args).notifiable?
    end

    def self.build_recipients(target, current_user, **args)
      ::NotificationRecipients::Builder::Default.new(target, current_user, **args).notification_recipients
    end

    def self.build_new_note_recipients(...)
      ::NotificationRecipients::Builder::NewNote.new(...).notification_recipients
    end

    def self.build_merge_request_unmergeable_recipients(...)
      ::NotificationRecipients::Builder::MergeRequestUnmergeable.new(...).notification_recipients
    end

    def self.build_project_maintainers_recipients(target, **args)
      ::NotificationRecipients::Builder::ProjectMaintainers.new(target, **args).notification_recipients
    end

    def self.build_new_review_recipients(...)
      ::NotificationRecipients::Builder::NewReview.new(...).notification_recipients
    end

    def self.build_requested_review_recipients(...)
      ::NotificationRecipients::Builder::RequestReview.new(...).notification_recipients
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe NotificationRecipients::BuildService, feature_category: :team_planning do
  let(:service) { described_class }
  let(:assignee) { create(:user) }
  let(:project) { create(:project, :public) }
  let(:other_projects) { create_list(:project, 5, :public) }

  describe '#build_new_note_recipients' do
    let(:issue) { create(:issue, project: project, assignees: [assignee]) }
    let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id) }

    shared_examples 'no N+1 queries' do
      it 'avoids N+1 queries', :request_store do
        # existing N+1 due to multiple users having to be looked up in the project_authorizations table
        threshold = project.private? ? 1 : 0

        create_user

        service.build_new_note_recipients(note)

        control_count = ActiveRecord::QueryRecorder.new do
          service.build_new_note_recipients(note)
        end

        create_user

        expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count).with_threshold(threshold)
      end
    end

    context 'when there are multiple watchers' do
      def create_user
        watcher = create(:user)
        create(:notification_setting, source: project, user: watcher, level: :watch)

        other_projects.each do |other_project|
          create(:notification_setting, source: other_project, user: watcher, level: :watch)
        end
      end

      include_examples 'no N+1 queries'
    end

    context 'when there are multiple subscribers' do
      def create_user
        subscriber = create(:user)
        issue.subscriptions.create!(user: subscriber, project: project, subscribed: true)
      end

      include_examples 'no N+1 queries'

      context 'when the project is private' do
        before do
          project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
        end

        include_examples 'no N+1 queries'
      end
    end
  end

  describe '#build_new_review_recipients' do
    let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
    let(:review) { create(:review, merge_request: merge_request, project: project, author: merge_request.author) }
    let(:notes) { create_list(:note_on_merge_request, 3, review: review, noteable: review.merge_request, project: review.project) }

    shared_examples 'no N+1 queries' do
      it 'avoids N+1 queries', :request_store do
        # existing N+1 due to multiple users having to be looked up in the project_authorizations table
        threshold = project.private? ? 1 : 0

        create_user

        service.build_new_review_recipients(review)

        control_count = ActiveRecord::QueryRecorder.new do
          service.build_new_review_recipients(review)
        end

        create_user

        expect { service.build_new_review_recipients(review) }.not_to exceed_query_limit(control_count).with_threshold(threshold)
      end
    end

    context 'when there are multiple watchers' do
      def create_user
        watcher = create(:user)
        create(:notification_setting, source: project, user: watcher, level: :watch)

        other_projects.each do |other_project|
          create(:notification_setting, source: other_project, user: watcher, level: :watch)
        end
      end

      include_examples 'no N+1 queries'
    end

    context 'when there are multiple subscribers' do
      def create_user
        subscriber = create(:user)
        merge_request.subscriptions.create!(user: subscriber, project: project, subscribed: true)
      end

      include_examples 'no N+1 queries'

      context 'when the project is private' do
        before do
          project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
        end

        include_examples 'no N+1 queries'
      end
    end
  end

  describe '#build_requested_review_recipients' do
    let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }

    before do
      merge_request.reviewers.push(assignee)
    end

    shared_examples 'no N+1 queries' do
      it 'avoids N+1 queries', :request_store do
        create_user

        service.build_requested_review_recipients(note)

        control_count = ActiveRecord::QueryRecorder.new do
          service.build_requested_review_recipients(note)
        end

        create_user

        expect { service.build_requested_review_recipients(note) }.not_to exceed_query_limit(control_count)
      end
    end
  end
end


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

module NotificationRecipients
  module Builder
    class NewNote < Base
      attr_reader :note

      def initialize(note)
        @note = note
      end

      def target
        note.noteable
      end

      def recipients_target
        note
      end

      # NOTE: may be nil, in the case of a PersonalSnippet
      #
      # (this is okay because NotificationRecipient is written
      # to handle nil projects)
      def project
        note.project
      end

      def group
        if note.for_project_noteable?
          project.group
        else
          target.try(:group)
        end
      end

      def build!
        # Add all users participating in the thread (author, assignee, comment authors)
        add_participants(note.author)
        add_mentions(note.author, target: note)

        if note.for_project_noteable?
          # Merge project watchers
          add_project_watchers
        else
          add_group_watchers
        end

        add_custom_notifications
        add_subscribed_users
      end

      def custom_action
        :new_note
      end

      def acting_user
        note.author
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe NotificationRecipients::Builder::NewNote, feature_category: :team_planning do
  describe '#notification_recipients' do
    let_it_be(:group)   { create(:group, :public) }
    let_it_be(:project) { create(:project, :public, group: group) }
    let_it_be(:issue) { create(:issue, project: project) }

    let_it_be(:other_user)              { create(:user) }
    let_it_be(:participant)             { create(:user) }
    let_it_be(:non_member_participant)  { create(:user) }
    let_it_be(:group_watcher)           { create(:user) }
    let_it_be(:project_watcher)         { create(:user) }
    let_it_be(:guest_project_watcher)   { create(:user) }
    let_it_be(:subscriber)              { create(:user) }
    let_it_be(:unsubscribed_user)       { create(:user) }
    let_it_be(:non_member_subscriber)   { create(:user) }

    let_it_be(:notification_setting_project_w) { create(:notification_setting, source: project, user: project_watcher, level: 2) }
    let_it_be(:notification_setting_guest_w) { create(:notification_setting, source: project, user: guest_project_watcher, level: 2) }
    let_it_be(:notification_setting_group_w) { create(:notification_setting, source: group, user: group_watcher, level: 2) }
    let_it_be(:subscriptions) do
      [
        create(:subscription, project: project, user: subscriber, subscribable: issue, subscribed: true),
        create(:subscription, project: project, user: unsubscribed_user, subscribable: issue, subscribed: false),
        create(:subscription, project: project, user: non_member_subscriber, subscribable: issue, subscribed: true)
      ]
    end

    subject { described_class.new(note) }

    before do
      project.add_developer(participant)
      project.add_developer(project_watcher)
      project.add_guest(guest_project_watcher)
      project.add_developer(subscriber)
      group.add_developer(group_watcher)

      expect(issue).to receive(:participants).and_return([participant, non_member_participant])
    end

    context 'for public notes' do
      let_it_be(:note) { create(:note, noteable: issue, project: project) }

      it 'adds all participants, watchers and subscribers' do
        expect(subject.notification_recipients.map(&:user)).to contain_exactly(
          participant, non_member_participant, project_watcher, group_watcher, guest_project_watcher, subscriber, non_member_subscriber
        )
      end
    end

    context 'for confidential notes' do
      let_it_be(:note) { create(:note, :confidential, noteable: issue, project: project) }

      it 'adds all participants, watchers and subscribers that are project memebrs' do
        expect(subject.notification_recipients.map(&:user)).to contain_exactly(
          participant, project_watcher, group_watcher, subscriber
        )
      end
    end
  end
end


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

module NotificationRecipients
  module Builder
    class Default < Base
      MENTION_TYPE_ACTIONS = [:new_issue, :new_merge_request].freeze

      attr_reader :target
      attr_reader :current_user
      attr_reader :action
      attr_reader :previous_assignees
      attr_reader :skip_current_user

      def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true)
        @target = target
        @current_user = current_user
        @action = action
        @custom_action = custom_action
        @previous_assignees = previous_assignees
        @skip_current_user = skip_current_user
      end

      def add_watchers
        add_project_watchers
      end

      def build!
        add_participants(current_user)
        add_watchers
        add_custom_notifications

        # Re-assign is considered as a mention of the new assignee
        case custom_action
        when :reassign_merge_request, :reassign_issue
          add_recipients(previous_assignees, :mention, nil)
          add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
        when :change_reviewer_merge_request
          add_recipients(previous_assignees, :mention, nil)
          add_recipients(target.reviewers, :mention, NotificationReason::REVIEW_REQUESTED)
        end

        add_subscribed_users

        if self.class.mention_type_actions.include?(custom_action)
          # These will all be participants as well, but adding with the :mention
          # type ensures that users with the mention notification level will
          # receive them, too.
          add_mentions(current_user, target: target)

          # We use the `:participating` notification level in order to match existing legacy behavior as captured
          # in existing specs (notification_service_spec.rb ~ line 507)
          if target.is_a?(Issuable)
            add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED)
          end

          add_labels_subscribers
        end
      end

      def acting_user
        current_user if skip_current_user
      end

      # Build event key to search on custom notification level
      # Check NotificationSetting.email_events
      def custom_action
        @custom_action ||= "#{action}_#{target.class.model_name.name.underscore}".to_sym
      end

      def self.mention_type_actions
        MENTION_TYPE_ACTIONS.dup
      end
    end
  end
end

NotificationRecipients::Builder::Default.prepend_mod_with('NotificationRecipients::Builder::Default')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe NotificationRecipients::Builder::Default, feature_category: :team_planning do
  describe '#build!' do
    let_it_be(:group)   { create(:group, :public) }
    let_it_be(:project) { create(:project, :public, group: group).tap { |p| p.add_developer(project_watcher) if project_watcher } }
    let_it_be(:target)  { create(:issue, project: project) }

    let_it_be(:current_user)    { create(:user) }
    let_it_be(:other_user)      { create(:user) }
    let_it_be(:participant)     { create(:user) }
    let_it_be(:group_watcher)   { create(:user) }
    let_it_be(:project_watcher) { create(:user) }

    let_it_be(:notification_setting_project_w) { create(:notification_setting, source: project, user: project_watcher, level: 2) }
    let_it_be(:notification_setting_group_w) { create(:notification_setting, source: group, user: group_watcher, level: 2) }

    subject { described_class.new(target, current_user, action: :new).tap { |s| s.build! } }

    context 'participants and project watchers' do
      before do
        expect(target).to receive(:participants).and_return([participant, current_user])
      end

      it 'adds all participants and watchers' do
        expect(subject.recipients.map(&:user)).to include(participant, project_watcher, group_watcher)
        expect(subject.recipients.map(&:user)).not_to include(other_user)
      end
    end

    context 'subscribers' do
      it 'adds all subscribers' do
        subscriber = create(:user)
        non_subscriber = create(:user)
        create(:subscription, project: project, user: subscriber, subscribable: target, subscribed: true)
        create(:subscription, project: project, user: non_subscriber, subscribable: target, subscribed: false)

        expect(subject.recipients.map(&:user)).to include(subscriber)
      end
    end

    context 'custom notifications' do
      shared_examples 'custom notification recipients' do
        let_it_be(:custom_notification_user) { create(:user) }
        let_it_be(:another_group)   { create(:group) }
        let_it_be(:another_project) { create(:project, namespace: another_group) }

        context 'with project custom notification setting' do
          before do
            create(:notification_setting, source: project, user: custom_notification_user, level: :custom)
          end

          it 'adds the user to the recipients' do
            expect(subject.recipients.map(&:user)).to include(custom_notification_user)
          end
        end

        context 'with the project custom notification setting in another project' do
          before do
            create(:notification_setting, source: another_project, user: custom_notification_user, level: :custom)
          end

          it 'does not add the user to the recipients' do
            expect(subject.recipients.map(&:user)).not_to include(custom_notification_user)
          end
        end

        context 'with group custom notification setting' do
          before do
            create(:notification_setting, source: group, user: custom_notification_user, level: :custom)
          end

          it 'adds the user to the recipients' do
            expect(subject.recipients.map(&:user)).to include(custom_notification_user)
          end
        end

        context 'with the group custom notification setting in another group' do
          before do
            create(:notification_setting, source: another_group, user: custom_notification_user, level: :custom)
          end

          it 'does not add the user to the recipients' do
            expect(subject.recipients.map(&:user)).not_to include(custom_notification_user)
          end
        end

        context 'with project global custom notification setting' do
          before do
            create(:notification_setting, source: project, user: custom_notification_user, level: :global)
          end

          context 'with global custom notification setting' do
            before do
              create(:notification_setting, source: nil, user: custom_notification_user, level: :custom)
            end

            it 'adds the user to the recipients' do
              expect(subject.recipients.map(&:user)).to include(custom_notification_user)
            end
          end

          context 'without global custom notification setting' do
            it 'does not add the user to the recipients' do
              expect(subject.recipients.map(&:user)).not_to include(custom_notification_user)
            end
          end
        end

        context 'with group global custom notification setting' do
          before do
            create(:notification_setting, source: group, user: custom_notification_user, level: :global)
          end

          context 'with global custom notification setting' do
            before do
              create(:notification_setting, source: nil, user: custom_notification_user, level: :custom)
            end

            it 'adds the user to the recipients' do
              expect(subject.recipients.map(&:user)).to include(custom_notification_user)
            end
          end

          context 'without global custom notification setting' do
            it 'does not add the user to the recipients' do
              expect(subject.recipients.map(&:user)).not_to include(custom_notification_user)
            end
          end
        end

        context 'with group custom notification setting in deeply nested parent group' do
          let(:grand_parent_group) { create(:group, :public) }
          let(:parent_group) { create(:group, :public, parent: grand_parent_group) }
          let(:group) { create(:group, :public, parent: parent_group) }
          let(:project) { create(:project, :public, group: group).tap { |p| p.add_developer(project_watcher) } }
          let(:target) { create(:issue, project: project) }

          before do
            create(:notification_setting, source: grand_parent_group, user: custom_notification_user, level: :custom)
          end

          it 'adds the user to the recipients' do
            expect(subject.recipients.map(&:user)).to include(custom_notification_user)
          end
        end

        context 'without a project or group' do
          let(:target) { create(:snippet) }

          before do
            create(:notification_setting, source: nil, user: custom_notification_user, level: :custom)
          end

          it 'does not add the user to the recipients' do
            expect(subject.recipients.map(&:user)).not_to include(custom_notification_user)
          end
        end
      end

      it_behaves_like 'custom notification recipients'
    end
  end
end


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

module JiraConnectInstallations
  class ProxyLifecycleEventService
    SUPPOERTED_EVENTS = %i[installed uninstalled].freeze

    def self.execute(installation, event, instance_url)
      new(installation, event, instance_url).execute
    end

    def initialize(installation, event, instance_url)
      # To ensure the event is sent to the right instance, this makes
      # a copy of the installation and assigns the instance_url
      #
      # The installation might be modified already with a new instance_url.
      # This can be the case for an uninstalled event.
      # The installation is updated first, and the uninstalled event has to be sent to
      # the old instance_url.
      @installation = installation.dup
      @installation.instance_url = instance_url

      @event = event.to_sym

      raise(ArgumentError, "Unknown event '#{@event}'") unless SUPPOERTED_EVENTS.include?(@event)
    end

    def execute
      result = send_hook

      return ServiceResponse.new(status: :success) if result.code == 200

      log_unsuccessful_response(result.code, result.body)

      ServiceResponse.error(message: { type: :response_error, code: result.code })
    rescue *Gitlab::HTTP::HTTP_ERRORS => error
      ServiceResponse.error(message: { type: :network_error, message: error.message })
    end

    private

    attr_reader :installation, :event

    def send_hook
      Gitlab::HTTP.post(hook_uri, body: body)
    end

    def hook_uri
      case event
      when :installed
        installation.audience_installed_event_url
      when :uninstalled
        installation.audience_uninstalled_event_url
      end
    end

    def body
      return base_body unless event == :installed

      base_body.merge(installed_body)
    end

    def base_body
      {
        clientKey: installation.client_key,
        jwt: jwt_token,
        eventType: event
      }
    end

    def installed_body
      {
        sharedSecret: installation.shared_secret,
        baseUrl: installation.base_url
      }
    end

    def jwt_token
      @jwt_token ||= JiraConnect::CreateAsymmetricJwtService.new(@installation, event: event).execute
    end

    def log_unsuccessful_response(status_code, body)
      Gitlab::IntegrationsLogger.info(
        integration: 'JiraConnect',
        message: 'Proxy lifecycle event received error response',
        jira_event_type: event,
        jira_status_code: status_code,
        jira_body: body
      )
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe JiraConnectInstallations::ProxyLifecycleEventService, feature_category: :integrations do
  describe '.execute' do
    let(:installation) { create(:jira_connect_installation) }

    it 'creates an instance and calls execute' do
      expect_next_instance_of(described_class, installation, 'installed', 'https://test.gitlab.com') do |update_service|
        expect(update_service).to receive(:execute)
      end

      described_class.execute(installation, 'installed', 'https://test.gitlab.com')
    end
  end

  describe '.new' do
    let_it_be(:installation) { create(:jira_connect_installation, instance_url: nil) }

    let(:event) { :installed }

    subject(:service) { described_class.new(installation, event, 'https://test.gitlab.com') }

    it 'creates an internal duplicate of the installation and sets the instance_url' do
      expect(service.instance_variable_get(:@installation).instance_url).to eq('https://test.gitlab.com')
    end

    context 'with unknown event' do
      let(:event) { 'test' }

      it 'raises an error' do
        expect { service }.to raise_error(ArgumentError, 'Unknown event \'test\'')
      end
    end
  end

  describe '#execute' do
    let_it_be(:installation) { create(:jira_connect_installation, instance_url: 'https://old_instance_url.example.com') }

    let(:service) { described_class.new(installation, evnet_type, 'https://gitlab.example.com') }
    let(:service_instance_installation) { service.instance_variable_get(:@installation) }

    before do
      allow_next_instance_of(JiraConnect::CreateAsymmetricJwtService) do |create_asymmetric_jwt_service|
        allow(create_asymmetric_jwt_service).to receive(:execute).and_return('123456')
      end

      stub_request(:post, hook_url)
    end

    subject(:execute_service) { service.execute }

    shared_examples 'sends the event hook' do
      it 'returns a ServiceResponse' do
        expect(execute_service).to be_kind_of(ServiceResponse)
        expect(execute_service[:status]).to eq(:success)
      end

      it 'sends an installed event to the instance' do
        execute_service

        expect(WebMock).to have_requested(:post, hook_url).with(body: expected_request_body)
      end

      it 'creates the JWT token with the event and installation' do
        expect_next_instance_of(
          JiraConnect::CreateAsymmetricJwtService,
          service_instance_installation,
          event: evnet_type
        ) do |create_asymmetric_jwt_service|
          expect(create_asymmetric_jwt_service).to receive(:execute).and_return('123456')
        end

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

      context 'and the instance responds with an error' do
        before do
          stub_request(:post, hook_url).to_return(
            status: 422,
            body: 'Error message',
            headers: {}
          )
        end

        it 'returns an error ServiceResponse', :aggregate_failures do
          expect(execute_service).to be_kind_of(ServiceResponse)
          expect(execute_service[:status]).to eq(:error)
          expect(execute_service[:message]).to eq( { type: :response_error, code: 422 } )
        end

        it 'logs the error response' do
          expect(Gitlab::IntegrationsLogger).to receive(:info).with(
            integration: 'JiraConnect',
            message: 'Proxy lifecycle event received error response',
            jira_event_type: evnet_type,
            jira_status_code: 422,
            jira_body: 'Error message'
          )

          execute_service
        end
      end

      context 'and the request raises an error' do
        before do
          allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
        end

        it 'returns an error ServiceResponse', :aggregate_failures do
          expect(execute_service).to be_kind_of(ServiceResponse)
          expect(execute_service[:status]).to eq(:error)
          expect(execute_service[:message]).to eq(
            {
              type: :network_error,
              message: 'Connection refused - error message'
            }
          )
        end
      end
    end

    context 'when installed event' do
      let(:evnet_type) { :installed }
      let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/installed' }
      let(:expected_request_body) do
        {
          clientKey: installation.client_key,
          sharedSecret: installation.shared_secret,
          baseUrl: installation.base_url,
          jwt: '123456',
          eventType: 'installed'
        }
      end

      it_behaves_like 'sends the event hook'
    end

    context 'when uninstalled event' do
      let(:evnet_type) { :uninstalled }
      let(:hook_url) { 'https://gitlab.example.com/-/jira_connect/events/uninstalled' }
      let(:expected_request_body) do
        {
          clientKey: installation.client_key,
          jwt: '123456',
          eventType: 'uninstalled'
        }
      end

      it_behaves_like 'sends the event hook'
    end
  end
end


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

module JiraConnectInstallations
  class DestroyService
    def self.execute(installation, jira_connect_base_path, jira_connect_uninstalled_event_path)
      new(installation, jira_connect_base_path, jira_connect_uninstalled_event_path).execute
    end

    def initialize(installation, jira_connect_base_path, jira_connect_uninstalled_event_path)
      @installation = installation
      @jira_connect_base_path = jira_connect_base_path
      @jira_connect_uninstalled_event_path = jira_connect_uninstalled_event_path
    end

    def execute
      if @installation.instance_url.present?
        JiraConnect::ForwardEventWorker.perform_async(@installation.id, @jira_connect_base_path, @jira_connect_uninstalled_event_path)
        return true
      end

      @installation.destroy
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe JiraConnectInstallations::DestroyService, feature_category: :integrations do
  describe '.execute' do
    it 'creates an instance and calls execute' do
      expect_next_instance_of(described_class, 'param1', 'param2', 'param3') do |destroy_service|
        expect(destroy_service).to receive(:execute)
      end

      described_class.execute('param1', 'param2', 'param3')
    end
  end

  describe '#execute' do
    let!(:installation) { create(:jira_connect_installation) }
    let(:jira_base_path) { '/-/jira_connect' }
    let(:jira_event_path) { '/-/jira_connect/events/uninstalled' }

    subject { described_class.new(installation, jira_base_path, jira_event_path).execute }

    it { is_expected.to be_truthy }

    it 'deletes the installation' do
      expect { subject }.to change(JiraConnectInstallation, :count).by(-1)
    end

    context 'and the installation has an instance_url set' do
      let!(:installation) { create(:jira_connect_installation, instance_url: 'http://example.com') }

      it { is_expected.to be_truthy }

      it 'schedules a ForwardEventWorker background job and keeps the installation' do
        expect(JiraConnect::ForwardEventWorker).to receive(:perform_async).with(installation.id, jira_base_path, jira_event_path)

        expect { subject }.not_to change(JiraConnectInstallation, :count)
      end
    end
  end
end


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

module JiraConnectInstallations
  class UpdateService
    def self.execute(installation, update_params)
      new(installation, update_params).execute
    end

    def initialize(installation, update_params)
      @installation = installation
      @update_params = update_params
    end

    def execute
      return update_error unless @installation.update(@update_params)

      if @installation.instance_url?
        hook_result = ProxyLifecycleEventService.execute(@installation, :installed, @installation.instance_url)

        if instance_url_changed? && hook_result.error?
          @installation.update!(instance_url: @installation.instance_url_before_last_save)

          return instance_installation_creation_error(hook_result.message)
        end
      end

      send_uninstalled_hook if instance_url_changed? && @installation.instance_url.blank?

      ServiceResponse.new(status: :success)
    end

    private

    def instance_url_changed?
      @installation.instance_url_before_last_save != @installation.instance_url
    end

    def send_uninstalled_hook
      return if @installation.instance_url_before_last_save.blank?

      JiraConnect::SendUninstalledHookWorker.perform_async(
        @installation.id,
        @installation.instance_url_before_last_save
      )
    end

    def instance_installation_creation_error(error_message)
      message = if error_message[:type] == :response_error
                  "Could not be installed on the instance. Error response code #{error_message[:code]}"
                else
                  'Could not be installed on the instance. Network error'
                end

      ServiceResponse.error(message: message)
    end

    def update_error
      ServiceResponse.error(message: @installation.errors)
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integrations do
  describe '.execute' do
    it 'creates an instance and calls execute' do
      expect_next_instance_of(described_class, 'param1', 'param2') do |update_service|
        expect(update_service).to receive(:execute)
      end

      described_class.execute('param1', 'param2')
    end
  end

  describe '#execute' do
    let_it_be_with_reload(:installation) { create(:jira_connect_installation) }
    let(:update_params) { { client_key: 'new_client_key' } }

    subject(:execute_service) { described_class.new(installation, update_params).execute }

    it 'returns a ServiceResponse' do
      expect(execute_service).to be_kind_of(ServiceResponse)
      expect(execute_service[:status]).to eq(:success)
    end

    it 'updates the installation' do
      expect { execute_service }.to change { installation.client_key }.to('new_client_key')
    end

    it 'returns a successful result' do
      expect(execute_service.success?).to eq(true)
    end

    context 'and model validation fails' do
      let(:update_params) { { instance_url: 'invalid' } }

      it 'returns an error result' do
        expect(execute_service.error?).to eq(true)
        expect(execute_service.message).to eq(installation.errors)
      end
    end

    context 'and the installation has an instance_url' do
      let_it_be_with_reload(:installation) { create(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') }

      it 'sends an installed event to the instance', :aggregate_failures do
        expect_next_instance_of(
          JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://other_gitlab.example.com'
        ) do |proxy_lifecycle_events_service|
          expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
        end

        expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)

        expect { execute_service }.not_to change { installation.instance_url }
      end

      context 'and instance_url gets updated' do
        let(:update_params) { { instance_url: 'https://gitlab.example.com' } }

        before do
          stub_request(:post, 'https://other_gitlab.example.com/-/jira_connect/events/uninstalled')
        end

        it 'sends an installed event to the instance and updates instance_url' do
          expect(JiraConnectInstallations::ProxyLifecycleEventService)
            .to receive(:execute).with(installation, :installed, 'https://gitlab.example.com')
            .and_return(ServiceResponse.new(status: :success))

          expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)

          execute_service

          expect(installation.instance_url).to eq(update_params[:instance_url])
        end

        context 'and the new instance_url is nil' do
          let(:update_params) { { instance_url: nil } }

          it 'starts an async worker to send an uninstalled event to the previous instance' do
            expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com')

            execute_service

            expect(installation.instance_url).to eq(nil)
          end

          it 'does not send an installed event' do
            expect(JiraConnectInstallations::ProxyLifecycleEventService).not_to receive(:new)

            execute_service
          end
        end
      end
    end

    context 'and instance_url is updated' do
      let(:update_params) { { instance_url: 'https://gitlab.example.com' } }

      it 'sends an installed event to the instance and updates instance_url' do
        expect_next_instance_of(
          JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://gitlab.example.com'
        ) do |proxy_lifecycle_events_service|
          expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success))
        end

        expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)

        execute_service

        expect(installation.instance_url).to eq(update_params[:instance_url])
      end

      context 'and the instance installation cannot be created' do
        before do
          allow_next_instance_of(
            JiraConnectInstallations::ProxyLifecycleEventService,
            installation,
            :installed,
            'https://gitlab.example.com'
          ) do |proxy_lifecycle_events_service|
            allow(proxy_lifecycle_events_service).to receive(:execute).and_return(
              ServiceResponse.error(
                message: {
                  type: :response_error,
                  code: '422'
                }
              )
            )
          end
        end

        it 'does not change instance_url' do
          expect { execute_service }.not_to change { installation.instance_url }
        end

        it 'returns an error message' do
          expect(execute_service[:status]).to eq(:error)
          expect(execute_service[:message]).to eq("Could not be installed on the instance. Error response code 422")
        end

        context 'and the installation had a previous instance_url' do
          let(:installation) { build(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') }

          it 'does not send the uninstalled hook to the previous instance_url' do
            expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async)

            execute_service
          end
        end

        context 'when failure because of a network error' do
          before do
            allow_next_instance_of(
              JiraConnectInstallations::ProxyLifecycleEventService,
              installation,
              :installed,
              'https://gitlab.example.com'
            ) do |proxy_lifecycle_events_service|
              allow(proxy_lifecycle_events_service).to receive(:execute).and_return(
                ServiceResponse.error(
                  message: {
                    type: :network_error,
                    message: 'Connection refused - error message'
                  }
                )
              )
            end
          end

          it 'returns an error message' do
            expect(execute_service[:status]).to eq(:error)
            expect(execute_service[:message]).to eq("Could not be installed on the instance. Network error")
          end
        end
      end
    end
  end
end


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

module JiraConnectSubscriptions
  class CreateService < ::JiraConnectSubscriptions::BaseService
    include Gitlab::Utils::StrongMemoize
    MERGE_REQUEST_SYNC_BATCH_SIZE = 20
    MERGE_REQUEST_SYNC_BATCH_DELAY = 1.minute.freeze

    def execute
      if !params[:jira_user]
        return error(s_('JiraConnect|Could not fetch user information from Jira. ' \
                        'Check the permissions in Jira and try again.'), 403)
      elsif !can_administer_jira?
        return error(s_('JiraConnect|The Jira user is not a site or organization administrator. ' \
                        'Check the permissions in Jira and try again.'), 403)
      end

      unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
        return error(s_('JiraConnect|Cannot find namespace. Make sure you have sufficient permissions.'), 401)
      end

      create_subscription
    end

    private

    def can_administer_jira?
      params[:jira_user]&.jira_admin?
    end

    def create_subscription
      subscription = JiraConnectSubscription.new(installation: jira_connect_installation, namespace: namespace)

      if subscription.save
        schedule_sync_project_jobs

        success
      else
        error(subscription.errors.full_messages.join(', '), 422)
      end
    end

    def namespace
      strong_memoize(:namespace) do
        Namespace.find_by_full_path(params[:namespace_path])
      end
    end

    def schedule_sync_project_jobs
      namespace.all_projects.each_batch(of: MERGE_REQUEST_SYNC_BATCH_SIZE) do |projects, index|
        JiraConnect::SyncProjectWorker.bulk_perform_in_with_contexts(
          index * MERGE_REQUEST_SYNC_BATCH_DELAY,
          projects,
          arguments_proc: -> (project) { [project.id, Atlassian::JiraConnect::Client.generate_update_sequence_id] },
          context_proc: -> (project) { { project: project } }
        )
      end
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe JiraConnectSubscriptions::CreateService, feature_category: :integrations do
  let_it_be(:installation) { create(:jira_connect_installation) }
  let_it_be(:current_user) { create(:user) }
  let_it_be(:group) { create(:group) }

  let(:path) { group.full_path }
  let(:params) { { namespace_path: path, jira_user: jira_user } }
  let(:jira_user) { double(:JiraUser, jira_admin?: true) }

  subject { described_class.new(installation, current_user, params).execute }

  before do
    group.add_maintainer(current_user)
  end

  shared_examples 'a failed execution' do |**status_attributes|
    it 'does not create a subscription' do
      expect { subject }.not_to change { installation.subscriptions.count }
    end

    it 'returns an error status' do
      expect(subject[:status]).to eq(:error)
      expect(subject).to include(status_attributes)
    end
  end

  context 'remote user does not have access' do
    let(:jira_user) { double(jira_admin?: false) }

    it_behaves_like 'a failed execution',
      http_status: 403,
      message: 'The Jira user is not a site or organization administrator. Check the permissions in Jira and try again.'
  end

  context 'remote user cannot be retrieved' do
    let(:jira_user) { nil }

    it_behaves_like 'a failed execution',
      http_status: 403,
      message: 'Could not fetch user information from Jira. Check the permissions in Jira and try again.'
  end

  context 'when user does have access' do
    it 'creates a subscription' do
      expect { subject }.to change { installation.subscriptions.count }.from(0).to(1)
    end

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

    context 'namespace has projects' do
      let_it_be(:project_1) { create(:project, group: group) }
      let_it_be(:project_2) { create(:project, group: group) }

      before do
        stub_const("#{described_class}::MERGE_REQUEST_SYNC_BATCH_SIZE", 1)
      end

      it 'starts workers to sync projects in batches with delay' do
        allow(Atlassian::JiraConnect::Client).to receive(:generate_update_sequence_id).and_return(123)

        expect(JiraConnect::SyncProjectWorker).to receive(:bulk_perform_in).with(1.minute, [[project_1.id, 123]])
        expect(JiraConnect::SyncProjectWorker).to receive(:bulk_perform_in).with(2.minutes, [[project_2.id, 123]])

        subject
      end
    end
  end

  context 'when path is invalid' do
    let(:path) { 'some_invalid_namespace_path' }

    it_behaves_like 'a failed execution',
      http_status: 401,
      message: 'Cannot find namespace. Make sure you have sufficient permissions.'
  end

  context 'when user does not have access' do
    let_it_be(:other_group) { create(:group) }

    let(:path) { other_group.full_path }

    it_behaves_like 'a failed execution',
      http_status: 401,
      message: 'Cannot find namespace. Make sure you have sufficient permissions.'
  end
end


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

module Discussions
  class ResolveService < Discussions::BaseService
    include Gitlab::Utils::StrongMemoize

    def initialize(project, user = nil, params = {})
      @discussions = Array.wrap(params.fetch(:one_or_more_discussions))
      @follow_up_issue = params[:follow_up_issue]
      @resolved_count = 0

      raise ArgumentError, 'Discussions must be all for the same noteable' \
        unless noteable_is_same?

      super
    end

    def execute
      discussions.each { |discussion| resolve_discussion(discussion) }

      after_resolve_cleanup
    end

    private

    attr_accessor :discussions, :follow_up_issue

    def noteable_is_same?
      return true unless discussions.size > 1

      # Perform this check without fetching extra records
      discussions.all? do |discussion|
        discussion.noteable_type == first_discussion.noteable_type &&
          discussion.noteable_id == first_discussion.noteable_id
      end
    end

    def resolve_discussion(discussion)
      return unless discussion.can_resolve?(current_user)

      discussion.resolve!(current_user)
      @resolved_count += 1

      if merge_request
        Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
          .track_resolve_thread_action(user: current_user)

        MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
      end

      resolve_user_todos_for(discussion)
      SystemNoteService.discussion_continued_in_issue(discussion, project, current_user, follow_up_issue) if follow_up_issue
    end

    def resolve_user_todos_for(discussion)
      return unless discussion.for_design?

      TodoService.new.resolve_todos_for_target(discussion, current_user)
    end

    def first_discussion
      @first_discussion ||= discussions.first
    end

    def merge_request
      strong_memoize(:merge_request) do
        first_discussion.noteable if first_discussion.for_merge_request?
      end
    end

    def after_resolve_cleanup
      return unless merge_request
      return unless @resolved_count > 0

      send_graphql_triggers
      process_auto_merge
    end

    def send_graphql_triggers
      GraphqlTriggers.merge_request_merge_status_updated(merge_request)
    end

    def process_auto_merge
      return unless discussions_ready_to_merge?

      AutoMergeProcessWorker.perform_async(merge_request.id)
    end

    def discussions_ready_to_merge?
      merge_request.auto_merge_enabled? && merge_request.mergeable_discussions_state?
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Discussions::ResolveService, feature_category: :code_review_workflow do
  describe '#execute' do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:user) { create(:user, developer_projects: [project]) }
    let_it_be(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project) }

    let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
    let(:service) { described_class.new(project, user, one_or_more_discussions: discussion) }

    it "doesn't resolve discussions the user can't resolve" do
      expect(discussion).to receive(:can_resolve?).with(user).and_return(false)

      service.execute

      expect(discussion).not_to be_resolved
    end

    it 'resolves the discussion' do
      service.execute

      expect(discussion).to be_resolved
    end

    it 'tracks thread resolve usage data' do
      expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
        .to receive(:track_resolve_thread_action).with(user: user)

      service.execute
    end

    it 'executes the notification service' do
      expect_next_instance_of(MergeRequests::ResolvedDiscussionNotificationService) do |instance|
        expect(instance).to receive(:execute).with(discussion.noteable)
      end

      service.execute
    end

    it 'schedules an auto-merge' do
      expect(AutoMergeProcessWorker).to receive(:perform_async).with(discussion.noteable.id)

      service.execute
    end

    it 'sends GraphQL triggers' do
      expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(discussion.noteable)

      service.execute
    end

    context 'with a project that requires all discussion to be resolved' do
      before do
        project.update!(only_allow_merge_if_all_discussions_are_resolved: true)
      end

      after do
        project.update!(only_allow_merge_if_all_discussions_are_resolved: false)
      end

      let_it_be(:other_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }

      it 'does not schedule an auto-merge' do
        expect(AutoMergeProcessWorker).not_to receive(:perform_async)

        service.execute
      end

      it 'schedules an auto-merge' do
        expect(AutoMergeProcessWorker).to receive(:perform_async)

        described_class.new(project, user, one_or_more_discussions: [discussion, other_discussion]).execute
      end
    end

    it 'adds a system note to the discussion' do
      issue = create(:issue, project: project)

      expect(SystemNoteService).to receive(:discussion_continued_in_issue).with(discussion, project, user, issue)
      service = described_class.new(project, user, one_or_more_discussions: discussion, follow_up_issue: issue)
      service.execute
    end

    it 'can resolve multiple discussions at once' do
      other_discussion = create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion
      service = described_class.new(project, user, one_or_more_discussions: [discussion, other_discussion])
      service.execute

      expect([discussion, other_discussion]).to all(be_resolved)
    end

    it 'raises an argument error if discussions do not belong to the same noteable' do
      other_merge_request = create(:merge_request)
      other_discussion = create(
        :diff_note_on_merge_request,
        noteable: other_merge_request,
        project: other_merge_request.source_project
      ).to_discussion
      expect do
        described_class.new(project, user, one_or_more_discussions: [discussion, other_discussion])
      end.to raise_error(
        ArgumentError,
        'Discussions must be all for the same noteable'
      )
    end

    context 'when discussion is not for a merge request' do
      let_it_be(:design) { create(:design, :with_file, issue: create(:issue, project: project)) }

      let(:discussion) { create(:diff_note_on_design, noteable: design, project: project).to_discussion }

      it 'does not execute the notification service' do
        expect(MergeRequests::ResolvedDiscussionNotificationService).not_to receive(:new)

        service.execute
      end

      it 'does not track thread resolve usage data' do
        expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
          .not_to receive(:track_resolve_thread_action).with(user: user)

        service.execute
      end

      it 'does not schedule an auto-merge' do
        expect(AutoMergeProcessWorker).not_to receive(:perform_async)

        service.execute
      end

      it 'does not send GraphQL triggers' do
        expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated).with(discussion.noteable)

        service.execute
      end
    end

    context 'when resolving a discussion' do
      def resolve_discussion(discussion, user)
        described_class.new(project, user, one_or_more_discussions: discussion).execute
      end

      context 'in a design' do
        let_it_be(:design) { create(:design, :with_file, issue: create(:issue, project: project)) }
        let_it_be(:user_1) { create(:user) }
        let_it_be(:user_2) { create(:user) }
        let_it_be(:discussion_1) { create(:diff_note_on_design, noteable: design, project: project, author: user_1).to_discussion }
        let_it_be(:discussion_2) { create(:diff_note_on_design, noteable: design, project: project, author: user_2).to_discussion }

        before do
          project.add_developer(user_1)
          project.add_developer(user_2)
        end

        context 'when user resolving discussion has open todos' do
          let!(:user_1_todo_for_discussion_1) { create(:todo, :pending, user: user_1, target: design, note: discussion_1.notes.first, project: project) }
          let!(:user_1_todo_2_for_discussion_1) { create(:todo, :pending, user: user_1, target: design, note: discussion_1.notes.first, project: project) }
          let!(:user_1_todo_for_discussion_2) { create(:todo, :pending, user: user_1, target: design, note: discussion_2.notes.first, project: project) }
          let!(:user_2_todo_for_discussion_1) { create(:todo, :pending, user: user_2, target: design, note: discussion_1.notes.first, project: project) }

          it 'marks user todos for given discussion as done' do
            resolve_discussion(discussion_1, user_1)

            expect(user_1_todo_for_discussion_1.reload).to be_done
            expect(user_1_todo_2_for_discussion_1.reload).to be_done
            expect(user_1_todo_for_discussion_2.reload).to be_pending
            expect(user_2_todo_for_discussion_1.reload).to be_pending
          end
        end
      end

      context 'in a merge request' do
        let!(:user_todo_for_discussion) { create(:todo, :pending, user: user, target: merge_request, note: discussion.notes.first, project: project) }

        it 'does not mark user todo as done' do
          resolve_discussion(discussion, user)

          expect(user_todo_for_discussion).to be_pending
        end
      end
    end
  end
end


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

module Discussions
  class UnresolveService < Discussions::BaseService
    include Gitlab::Utils::StrongMemoize

    def initialize(discussion, user)
      @discussion = discussion
      @user = user

      super
    end

    def execute
      @all_discussions_resolved_before = merge_request ? @discussion.noteable.discussions_resolved? : false

      @discussion.unresolve!

      send_graphql_triggers

      Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter
        .track_unresolve_thread_action(user: @user)
    end

    private

    def merge_request
      @discussion.noteable if @discussion.for_merge_request?
    end
    strong_memoize_attr :merge_request

    def send_graphql_triggers
      return unless merge_request && @all_discussions_resolved_before

      GraphqlTriggers.merge_request_merge_status_updated(merge_request)
    end
  end
end

```

Response:
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Discussions::UnresolveService, feature_category: :code_review_workflow do
  describe "#execute" do
    let_it_be(:project) { create(:project, :repository) }
    let_it_be(:user) { create(:user, developer_projects: [project]) }
    let_it_be(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds, source_project: project) }

    let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }

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

    before do
      project.add_developer(user)
      discussion.resolve!(user)
    end

    it "unresolves the discussion" do
      service.execute

      expect(discussion).not_to be_resolved
    end

    it "counts the unresolve event" do
      expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
        .to receive(:track_unresolve_thread_action).with(user: user)

      service.execute
    end

    it "sends GraphQL triggers" do
      expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(discussion.noteable)

      service.execute
    end

    context "when there are existing unresolved discussions" do
      before do
        create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion
      end

      it "does not send a GraphQL triggers" do
        expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated)

        service.execute
      end
    end

    context "when the noteable is not a merge request" do
      it "does not send a GraphQL triggers" do
        expect(discussion).to receive(:for_merge_request?).and_return(false)
        expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated)

        service.execute
      end
    end
  end
end


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

module Discussions
  class UpdateDiffPositionService < BaseService
    def execute(discussion)
      old_position = discussion.position
      result = tracer.trace(old_position)
      return unless result

      position = result[:position]
      outdated = result[:outdated]

      discussion.notes.each do |note|
        if outdated
          note.change_position = position

          if project.resolve_outdated_diff_discussions?
            note.resolve_without_save(current_user, resolved_by_push: true)
          end
        else
          note.position = position
          note.change_position = nil
        end
      end

      Note.transaction do
        discussion.notes.each do |note|
          note.save(touch: false)
        end

        if outdated && current_user
          SystemNoteService.diff_discussion_outdated(discussion, project, current_user, position)
        end
      end
    end

    private

    def tracer
      @tracer ||= Gitlab::Diff::PositionTracer.new(
        project: project,
        old_diff_refs: params[:old_diff_refs],
        new_diff_refs: params[:new_diff_refs],
        paths: params[:paths]
      )
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Discussions::UpdateDiffPositionService, feature_category: :code_review_workflow do
  let(:project) { create(:project, :repository) }
  let(:current_user) { project.first_owner }
  let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
  let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
  let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }

  let(:path) { "files/ruby/popen.rb" }

  let(:old_diff_refs) do
    Gitlab::Diff::DiffRefs.new(
      base_sha: create_commit.parent_id,
      head_sha: modify_commit.sha
    )
  end

  let(:new_diff_refs) do
    Gitlab::Diff::DiffRefs.new(
      base_sha: create_commit.parent_id,
      head_sha: edit_commit.sha
    )
  end

  subject do
    described_class.new(
      project,
      current_user,
      old_diff_refs: old_diff_refs,
      new_diff_refs: new_diff_refs,
      paths: [path]
    )
  end

  # old diff:
  #     1 + require 'fileutils'
  #     2 + require 'open3'
  #     3 +
  #     4 + module Popen
  #     5 +   extend self
  #     6 +
  #     7 +   def popen(cmd, path=nil)
  #     8 +     unless cmd.is_a?(Array)
  #     9 +       raise "System commands must be given as an array of strings"
  #    10 +     end
  #    11 +
  #    12 +     path ||= Dir.pwd
  #    13 +     vars = { "PWD" => path }
  #    14 +     options = { chdir: path }
  #    15 +
  #    16 +     unless File.directory?(path)
  #    17 +       FileUtils.mkdir_p(path)
  #    18 +     end
  #    19 +
  #    20 +     @cmd_output = ""
  #    21 +     @cmd_status = 0
  #    22 +     Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  #    23 +       @cmd_output << stdout.read
  #    24 +       @cmd_output << stderr.read
  #    25 +       @cmd_status = wait_thr.value.exitstatus
  #    26 +     end
  #    27 +
  #    28 +     return @cmd_output, @cmd_status
  #    29 +   end
  #    30 + end
  #
  # new diff:
  #     1 + require 'fileutils'
  #     2 + require 'open3'
  #     3 +
  #     4 + module Popen
  #     5 +   extend self
  #     6 +
  #     7 +   def popen(cmd, path=nil)
  #     8 +     unless cmd.is_a?(Array)
  #     9 +       raise RuntimeError, "System commands must be given as an array of strings"
  #    10 +     end
  #    11 +
  #    12 +     path ||= Dir.pwd
  #    13 +
  #    14 +     vars = {
  #    15 +       "PWD" => path
  #    16 +     }
  #    17 +
  #    18 +     options = {
  #    19 +       chdir: path
  #    20 +     }
  #    21 +
  #    22 +     unless File.directory?(path)
  #    23 +       FileUtils.mkdir_p(path)
  #    24 +     end
  #    25 +
  #    26 +     @cmd_output = ""
  #    27 +     @cmd_status = 0
  #    28 +
  #    29 +     Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  #    30 +       @cmd_output << stdout.read
  #    31 +       @cmd_output << stderr.read
  #    32 +       @cmd_status = wait_thr.value.exitstatus
  #    33 +     end
  #    34 +
  #    35 +     return @cmd_output, @cmd_status
  #    36 +   end
  #    37 + end
  #
  # old->new diff:
  # .. .. @@ -6,12 +6,18 @@ module Popen
  #  6  6
  #  7  7    def popen(cmd, path=nil)
  #  8  8      unless cmd.is_a?(Array)
  #  9    -      raise "System commands must be given as an array of strings"
  #     9 +      raise RuntimeError, "System commands must be given as an array of strings"
  # 10 10      end
  # 11 11
  # 12 12      path ||= Dir.pwd
  # 13    -    vars = { "PWD" => path }
  # 14    -    options = { chdir: path }
  #    13 +
  #    14 +    vars = {
  #    15 +      "PWD" => path
  #    16 +    }
  #    17 +
  #    18 +    options = {
  #    19 +      chdir: path
  #    20 +    }
  # 15 21
  # 16 22      unless File.directory?(path)
  # 17 23        FileUtils.mkdir_p(path)
  # 18 24      end
  # 19 25
  # 20 26      @cmd_output = ""
  # 21 27      @cmd_status = 0
  #    28 +
  # 22 29      Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
  # 23 30        @cmd_output << stdout.read
  # 24 31        @cmd_output << stderr.read
  # .. ..

  describe "#execute" do
    let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion }

    let(:old_position) do
      Gitlab::Diff::Position.new(
        old_path: path,
        new_path: path,
        old_line: nil,
        new_line: line,
        diff_refs: old_diff_refs
      )
    end

    shared_examples 'outdated diff note' do
      it "doesn't update the position" do
        subject.execute(discussion)

        expect(discussion.original_position).to eq(old_position)
        expect(discussion.position).to eq(old_position)
      end

      it 'sets the change position' do
        subject.execute(discussion)

        change_position = discussion.change_position
        expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
        expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
        expect(change_position.formatter.old_line).to eq(9)
        expect(change_position.formatter.new_line).to be_nil
      end

      it 'creates a system discussion' do
        expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
          discussion, project, current_user, instance_of(Gitlab::Diff::Position))

        subject.execute(discussion)
      end
    end

    context "when the diff line is the same" do
      let(:line) { 16 }

      it "updates the position" do
        subject.execute(discussion)

        expect(discussion.original_position).to eq(old_position)
        expect(discussion.position).not_to eq(old_position)
        expect(discussion.position.formatter.new_line).to eq(22)
      end

      context 'when the resolve_outdated_diff_discussions setting is set' do
        before do
          project.update!(resolve_outdated_diff_discussions: true)
        end

        it 'does not resolve the discussion' do
          subject.execute(discussion)

          expect(discussion).not_to be_resolved
          expect(discussion).not_to be_resolved_by_push
        end
      end
    end

    context "when the diff line has changed" do
      let(:line) { 9 }

      include_examples 'outdated diff note'

      context 'when the resolve_outdated_diff_discussions setting is set' do
        before do
          project.update!(resolve_outdated_diff_discussions: true)
        end

        it 'sets resolves the discussion and sets resolved_by_push' do
          subject.execute(discussion)

          expect(discussion).to be_resolved
          expect(discussion).to be_resolved_by_push
        end

        include_examples 'outdated diff note'
      end
    end
  end
end


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

module Discussions
  class CaptureDiffNotePositionsService
    def initialize(merge_request)
      @merge_request = merge_request
    end

    def execute
      return unless merge_request.has_complete_diff_refs?

      discussions, paths = build_discussions

      service = Discussions::CaptureDiffNotePositionService.new(merge_request, paths)

      discussions.each do |discussion|
        service.execute(discussion)
      end
    end

    private

    attr_reader :merge_request

    def build_discussions
      active_diff_discussions = merge_request.notes.new_diff_notes.discussions.select do |discussion|
        discussion.active?(merge_request.diff_refs)
      end
      paths = active_diff_discussions.flat_map { |n| n.diff_file&.paths }

      [active_diff_discussions, paths]
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Discussions::CaptureDiffNotePositionsService, feature_category: :code_review_workflow do
  context 'when merge request has a discussion' do
    let(:source_branch) { 'compare-with-merge-head-source' }
    let(:target_branch) { 'compare-with-merge-head-target' }
    let(:merge_request) { create(:merge_request, source_branch: source_branch, target_branch: target_branch) }
    let(:project) { merge_request.project }

    let(:offset) { 30 }
    let(:first_new_line) { 508 }
    let(:second_new_line) { 521 }
    let(:third_removed_line) { 1240 }

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

    def build_position(diff_refs, new_line: nil, old_line: nil)
      path = 'files/markdown/ruby-style-guide.md'
      Gitlab::Diff::Position.new(
        old_path: path, new_path: path, new_line: new_line, old_line: old_line, diff_refs: diff_refs)
    end

    def note_for(new_line: nil, old_line: nil)
      position = build_position(merge_request.diff_refs, new_line: new_line, old_line: old_line)
      create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
    end

    def verify_diff_note_position!(note, new_line: nil, old_line: nil)
      id, removed_line, added_line = note.line_code.split('_')

      expect(note.diff_note_positions.size).to eq(1)

      diff_position = note.diff_note_positions.last
      diff_refs = Gitlab::Diff::DiffRefs.new(
        base_sha: merge_request.target_branch_sha,
        start_sha: merge_request.target_branch_sha,
        head_sha: merge_request.merge_ref_head.sha)

      expect(diff_position.line_code).to eq("#{id}_#{removed_line.to_i - offset}_#{added_line}")
      expect(diff_position.position).to eq(build_position(diff_refs, new_line: new_line, old_line: old_line))
    end

    let!(:first_discussion_note) { note_for(new_line: first_new_line) }
    let!(:second_discussion_note) { note_for(new_line: second_new_line) }
    let!(:third_discussion_note) { note_for(old_line: third_removed_line) }
    let!(:second_discussion_another_note) do
      create(:diff_note_on_merge_request,
        project: project,
        position: second_discussion_note.position,
        discussion_id: second_discussion_note.discussion_id,
        noteable: merge_request)
    end

    context 'and position of the discussion changed on target branch head' do
      it 'diff positions are created for the first notes of the discussions' do
        MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author).execute(merge_request)
        service.execute

        verify_diff_note_position!(first_discussion_note, new_line: first_new_line)
        verify_diff_note_position!(second_discussion_note, new_line: second_new_line)
        verify_diff_note_position!(third_discussion_note, old_line: third_removed_line - offset)

        expect(second_discussion_another_note.diff_note_positions).to be_empty
      end
    end
  end
end