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

class UpdateStatusService < BaseService
  include Redisable
  include LanguagesHelper

  class NoChangesSubmittedError < StandardError; end

  # @param [Status] status
  # @param [Integer] account_id
  # @param [Hash] options
  # @option options [Array<Integer>] :media_ids
  # @option options [Array<Hash>] :media_attributes
  # @option options [Hash] :poll
  # @option options [String] :text
  # @option options [String] :spoiler_text
  # @option options [Boolean] :sensitive
  # @option options [String] :language
  def call(status, account_id, options = {})
    @status                    = status
    @options                   = options
    @account_id                = account_id
    @media_attachments_changed = false
    @poll_changed              = false

    Status.transaction do
      create_previous_edit!
      update_media_attachments! if @options.key?(:media_ids)
      update_poll! if @options.key?(:poll)
      update_immediate_attributes!
      create_edit!
    end

    queue_poll_notifications!
    reset_preview_card!
    update_metadata!
    broadcast_updates!

    @status
  rescue NoChangesSubmittedError
    # For calls that result in no changes, swallow the error
    # but get back to the original state

    @status.reload
  end

  private

  def update_media_attachments!
    previous_media_attachments = @status.ordered_media_attachments.to_a
    next_media_attachments     = validate_media!
    added_media_attachments    = next_media_attachments - previous_media_attachments

    (@options[:media_attributes] || []).each do |attributes|
      media = next_media_attachments.find { |attachment| attachment.id == attributes[:id].to_i }
      next if media.nil?

      media.update!(attributes.slice(:thumbnail, :description, :focus))
      @media_attachments_changed ||= media.significantly_changed?
    end

    MediaAttachment.where(id: added_media_attachments.map(&:id)).update_all(status_id: @status.id)

    @status.ordered_media_attachment_ids = (@options[:media_ids] || []).map(&:to_i) & next_media_attachments.map(&:id)
    @media_attachments_changed ||= previous_media_attachments.map(&:id) != @status.ordered_media_attachment_ids
    @status.media_attachments.reload
  end

  def validate_media!
    return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)

    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4 || @options[:poll].present?

    media_attachments = @status.account.media_attachments.where(status_id: [nil, @status.id]).where(scheduled_status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i)).to_a

    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media_attachments.size > 1 && media_attachments.find(&:audio_or_video?)
    raise Mastodon::ValidationError, I18n.t('media_attachments.validations.not_ready') if media_attachments.any?(&:not_processed?)

    media_attachments
  end

  def update_poll!
    previous_poll        = @status.preloadable_poll
    @previous_expires_at = previous_poll&.expires_at

    if @options[:poll].present?
      poll = previous_poll || @status.account.polls.new(status: @status, votes_count: 0)

      # If for some reasons the options were changed, it invalidates all previous
      # votes, so we need to remove them
      @poll_changed = true if @options[:poll][:options] != poll.options || ActiveModel::Type::Boolean.new.cast(@options[:poll][:multiple]) != poll.multiple

      poll.options     = @options[:poll][:options]
      poll.hide_totals = @options[:poll][:hide_totals] || false
      poll.multiple    = @options[:poll][:multiple] || false
      poll.expires_in  = @options[:poll][:expires_in]
      poll.reset_votes! if @poll_changed
      poll.save!

      @status.poll_id = poll.id
    elsif previous_poll.present?
      previous_poll.destroy
      @poll_changed = true
      @status.poll_id = nil
    end

    @poll_changed = true if @previous_expires_at != @status.preloadable_poll&.expires_at
  end

  def update_immediate_attributes!
    @status.text         = @options[:text].presence || @options.delete(:spoiler_text) || '' if @options.key?(:text)
    @status.spoiler_text = @options[:spoiler_text] || '' if @options.key?(:spoiler_text)
    @status.sensitive    = @options[:sensitive] || @options[:spoiler_text].present? if @options.key?(:sensitive) || @options.key?(:spoiler_text)
    @status.language     = valid_locale_cascade(@options[:language], @status.language, @status.account.user&.preferred_posting_language, I18n.default_locale)

    # We raise here to rollback the entire transaction
    raise NoChangesSubmittedError unless significant_changes?

    @status.edited_at = Time.now.utc
    @status.save!
  end

  def reset_preview_card!
    return unless @status.text_previously_changed?

    @status.reset_preview_card!
    LinkCrawlWorker.perform_async(@status.id)
  end

  def update_metadata!
    ProcessHashtagsService.new.call(@status)
    ProcessMentionsService.new.call(@status)
  end

  def broadcast_updates!
    DistributionWorker.perform_async(@status.id, { 'update' => true })
    ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id)
  end

  def queue_poll_notifications!
    poll = @status.preloadable_poll

    # If the poll had no expiration date set but now has, or now has a sooner
    # expiration date, schedule a notification

    return unless poll.present? && poll.expires_at.present?

    PollExpirationNotifyWorker.remove_from_scheduled(poll.id) if @previous_expires_at.present? && @previous_expires_at > poll.expires_at
    PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
  end

  def create_previous_edit!
    # We only need to create a previous edit when no previous edits exist, e.g.
    # when the status has never been edited. For other cases, we always create
    # an edit, so the step can be skipped

    return if @status.edits.any?

    @status.snapshot!(at_time: @status.created_at, rate_limit: false)
  end

  def create_edit!
    @status.snapshot!(account_id: @account_id)
  end

  def significant_changes?
    @status.changed? || @poll_changed || @media_attachments_changed
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UpdateStatusService, type: :service do
  subject { described_class.new }

  context 'when nothing changes' do
    let!(:status) { Fabricate(:status, text: 'Foo', language: 'en') }

    before do
      allow(ActivityPub::DistributionWorker).to receive(:perform_async)
      subject.call(status, status.account_id, text: 'Foo')
    end

    it 'does not create an edit' do
      expect(status.reload.edits).to be_empty
    end

    it 'does not notify anyone' do
      expect(ActivityPub::DistributionWorker).to_not have_received(:perform_async)
    end
  end

  context 'when text changes' do
    let(:status) { Fabricate(:status, text: 'Foo') }
    let(:preview_card) { Fabricate(:preview_card) }

    before do
      PreviewCardsStatus.create(status: status, preview_card: preview_card)
      subject.call(status, status.account_id, text: 'Bar')
    end

    it 'updates text' do
      expect(status.reload.text).to eq 'Bar'
    end

    it 'resets preview card' do
      expect(status.reload.preview_card).to be_nil
    end

    it 'saves edit history' do
      expect(status.edits.pluck(:text)).to eq %w(Foo Bar)
    end
  end

  context 'when content warning changes' do
    let(:status) { Fabricate(:status, text: 'Foo', spoiler_text: '') }
    let(:preview_card) { Fabricate(:preview_card) }

    before do
      PreviewCardsStatus.create(status: status, preview_card: preview_card)
      subject.call(status, status.account_id, text: 'Foo', spoiler_text: 'Bar')
    end

    it 'updates content warning' do
      expect(status.reload.spoiler_text).to eq 'Bar'
    end

    it 'saves edit history' do
      expect(status.edits.pluck(:text, :spoiler_text)).to eq [['Foo', ''], ['Foo', 'Bar']]
    end
  end

  context 'when media attachments change' do
    let!(:status) { Fabricate(:status, text: 'Foo') }
    let!(:detached_media_attachment) { Fabricate(:media_attachment, account: status.account) }
    let!(:attached_media_attachment) { Fabricate(:media_attachment, account: status.account) }

    before do
      status.media_attachments << detached_media_attachment
      subject.call(status, status.account_id, text: 'Foo', media_ids: [attached_media_attachment.id])
    end

    it 'updates media attachments' do
      expect(status.ordered_media_attachments).to eq [attached_media_attachment]
    end

    it 'does not detach detached media attachments' do
      expect(detached_media_attachment.reload.status_id).to eq status.id
    end

    it 'attaches attached media attachments' do
      expect(attached_media_attachment.reload.status_id).to eq status.id
    end

    it 'saves edit history' do
      expect(status.edits.pluck(:ordered_media_attachment_ids)).to eq [[detached_media_attachment.id], [attached_media_attachment.id]]
    end
  end

  context 'when already-attached media changes' do
    let!(:status) { Fabricate(:status, text: 'Foo') }
    let!(:media_attachment) { Fabricate(:media_attachment, account: status.account, description: 'Old description') }

    before do
      status.media_attachments << media_attachment
      subject.call(status, status.account_id, text: 'Foo', media_ids: [media_attachment.id], media_attributes: [{ id: media_attachment.id, description: 'New description' }])
    end

    it 'does not detach media attachment' do
      expect(media_attachment.reload.status_id).to eq status.id
    end

    it 'updates the media attachment description' do
      expect(media_attachment.reload.description).to eq 'New description'
    end

    it 'saves edit history' do
      expect(status.edits.map { |edit| edit.ordered_media_attachments.map(&:description) }).to eq [['Old description'], ['New description']]
    end
  end

  context 'when poll changes', :sidekiq_fake do
    let(:account) { Fabricate(:account) }
    let!(:status) { Fabricate(:status, text: 'Foo', account: account, poll_attributes: { options: %w(Foo Bar), account: account, multiple: false, hide_totals: false, expires_at: 7.days.from_now }) }
    let!(:poll)   { status.poll }
    let!(:voter) { Fabricate(:account) }

    before do
      status.update(poll: poll)
      VoteService.new.call(voter, poll, [0])
      subject.call(status, status.account_id, text: 'Foo', poll: { options: %w(Bar Baz Foo), expires_in: 5.days.to_i })
    end

    it 'updates poll' do
      poll = status.poll.reload
      expect(poll.options).to eq %w(Bar Baz Foo)
    end

    it 'resets votes' do
      poll = status.poll.reload
      expect(poll.votes_count).to eq 0
      expect(poll.votes.count).to eq 0
      expect(poll.cached_tallies).to eq [0, 0, 0]
    end

    it 'saves edit history' do
      expect(status.edits.pluck(:poll_options)).to eq [%w(Foo Bar), %w(Bar Baz Foo)]
    end

    it 'requeues expiration notification' do
      poll = status.poll.reload
      expect(PollExpirationNotifyWorker).to have_enqueued_sidekiq_job(poll.id).at(poll.expires_at + 5.minutes)
    end
  end

  context 'when mentions in text change' do
    let!(:account) { Fabricate(:account) }
    let!(:alice) { Fabricate(:account, username: 'alice') }
    let!(:bob) { Fabricate(:account, username: 'bob') }
    let!(:status) { PostStatusService.new.call(account, text: 'Hello @alice') }

    before do
      subject.call(status, status.account_id, text: 'Hello @bob')
    end

    it 'changes mentions' do
      expect(status.active_mentions.pluck(:account_id)).to eq [bob.id]
    end

    it 'keeps old mentions as silent mentions' do
      expect(status.mentions.pluck(:account_id)).to contain_exactly(alice.id, bob.id)
    end
  end

  context 'when hashtags in text change' do
    let!(:account) { Fabricate(:account) }
    let!(:status) { PostStatusService.new.call(account, text: 'Hello #foo') }

    before do
      subject.call(status, status.account_id, text: 'Hello #bar')
    end

    it 'changes tags' do
      expect(status.tags.pluck(:name)).to eq %w(bar)
    end
  end

  it 'notifies ActivityPub about the update' do
    status = Fabricate(:status, text: 'Foo')
    allow(ActivityPub::DistributionWorker).to receive(:perform_async)
    subject.call(status, status.account_id, text: 'Bar')
    expect(ActivityPub::DistributionWorker).to have_received(:perform_async)
  end
end


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

class BootstrapTimelineService < BaseService
  def call(source_account)
    @source_account = source_account

    autofollow_inviter!
    notify_staff!
  end

  private

  def autofollow_inviter!
    return unless @source_account&.user&.invite&.autofollow?

    FollowService.new.call(@source_account, @source_account.user.invite.user.account)
  end

  def notify_staff!
    User.those_who_can(:manage_users).includes(:account).find_each do |user|
      LocalNotificationWorker.perform_async(user.account_id, @source_account.id, 'Account', 'admin.sign_up')
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BootstrapTimelineService, type: :service do
  subject { described_class.new }

  context 'when the new user has registered from an invite' do
    let(:service)    { instance_double(FollowService) }
    let(:autofollow) { false }
    let(:inviter)    { Fabricate(:user, confirmed_at: 2.days.ago) }
    let(:invite)     { Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now, autofollow: autofollow) }
    let(:new_user)   { Fabricate(:user, invite_code: invite.code) }

    before do
      allow(FollowService).to receive(:new).and_return(service)
      allow(service).to receive(:call)
    end

    context 'when the invite has auto-follow enabled' do
      let(:autofollow) { true }

      it 'calls FollowService to follow the inviter' do
        subject.call(new_user.account)
        expect(service).to have_received(:call).with(new_user.account, inviter.account)
      end
    end

    context 'when the invite does not have auto-follow enable' do
      let(:autofollow) { false }

      it 'calls FollowService to follow the inviter' do
        subject.call(new_user.account)
        expect(service).to_not have_received(:call)
      end
    end
  end
end


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

class ResolveAccountService < BaseService
  include DomainControlHelper
  include WebfingerHelper
  include Redisable
  include Lockable

  # Find or create an account record for a remote user. When creating,
  # look up the user's webfinger and fetch ActivityPub data
  # @param [String, Account] uri URI in the username@domain format or account record
  # @param [Hash] options
  # @option options [Boolean] :redirected Do not follow further Webfinger redirects
  # @option options [Boolean] :skip_webfinger Do not attempt any webfinger query or refreshing account data
  # @option options [Boolean] :skip_cache Get the latest data from origin even if cache is not due to update yet
  # @option options [Boolean] :suppress_errors When failing, return nil instead of raising an error
  # @return [Account]
  def call(uri, options = {})
    return if uri.blank?

    process_options!(uri, options)

    # First of all we want to check if we've got the account
    # record with the URI already, and if so, we can exit early

    return if domain_not_allowed?(@domain)

    @account ||= Account.find_remote(@username, @domain)

    return @account if @account&.local? || @domain.nil? || !webfinger_update_due?

    # At this point we are in need of a Webfinger query, which may
    # yield us a different username/domain through a redirect
    process_webfinger!(@uri)
    @domain = nil if TagManager.instance.local_domain?(@domain)

    # Because the username/domain pair may be different than what
    # we already checked, we need to check if we've already got
    # the record with that URI, again

    return if domain_not_allowed?(@domain)

    @account ||= Account.find_remote(@username, @domain)

    if gone_from_origin? && not_yet_deleted?
      queue_deletion!
      return
    end

    return @account if @account&.local? || gone_from_origin? || !webfinger_update_due?

    # Now it is certain, it is definitely a remote account, and it
    # either needs to be created, or updated from fresh data

    fetch_account!
  rescue Webfinger::Error => e
    Rails.logger.debug { "Webfinger query for #{@uri} failed: #{e}" }
    raise unless @options[:suppress_errors]
  end

  private

  def process_options!(uri, options)
    @options = { suppress_errors: true }.merge(options)

    if uri.is_a?(Account)
      @account  = uri
      @username = @account.username
      @domain   = @account.domain
    else
      @username, @domain = uri.strip.gsub(/\A@/, '').split('@')
    end

    @domain = if TagManager.instance.local_domain?(@domain)
                nil
              else
                TagManager.instance.normalize_domain(@domain)
              end

    @uri = [@username, @domain].compact.join('@')
  end

  def process_webfinger!(uri)
    @webfinger                           = webfinger!("acct:#{uri}")
    confirmed_username, confirmed_domain = split_acct(@webfinger.subject)

    if confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
      @username = confirmed_username
      @domain   = confirmed_domain
      return
    end

    # Account doesn't match, so it may have been redirected
    @webfinger         = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
    @username, @domain = split_acct(@webfinger.subject)

    raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
  rescue Webfinger::GoneError
    @gone = true
  end

  def split_acct(acct)
    acct.delete_prefix('acct:').split('@').tap do |parts|
      raise Webfinger::Error, 'Webfinger response is missing user or host value' unless parts.size == 2
    end
  end

  def fetch_account!
    return unless activitypub_ready?

    with_redis_lock("resolve:#{@username}@#{@domain}") do
      @account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
    end

    @account
  end

  def webfinger_update_due?
    return false if @options[:check_delivery_availability] && !DeliveryFailureTracker.available?(@domain)
    return false if @options[:skip_webfinger]

    @options[:skip_cache] || @account.nil? || @account.possibly_stale?
  end

  def activitypub_ready?
    ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
  end

  def actor_url
    @actor_url ||= @webfinger.link('self', 'href')
  end

  def gone_from_origin?
    @gone
  end

  def not_yet_deleted?
    @account.present? && !@account.local?
  end

  def queue_deletion!
    @account.suspend!(origin: :remote)
    AccountDeletionWorker.perform_async(@account.id, { 'reserve_username' => false, 'skip_activitypub' => true })
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ResolveAccountService, type: :service do
  subject { described_class.new }

  before do
    stub_request(:get, 'https://example.com/.well-known/host-meta').to_return(status: 404)
    stub_request(:get, 'https://quitter.no/avatar/7477-300-20160211190340.png').to_return(request_fixture('avatar.txt'))
    stub_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com').to_return(request_fixture('activitypub-webfinger.txt'))
    stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor.txt'))
    stub_request(:get, 'https://ap.example.com/users/foo.atom').to_return(request_fixture('activitypub-feed.txt'))
    stub_request(:get, %r{https://ap\.example\.com/users/foo/\w+}).to_return(status: 404)
    stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:hoge@example.com').to_return(status: 410)
  end

  context 'when using skip_webfinger' do
    context 'when account is known' do
      let!(:remote_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', protocol: 'activitypub') }

      context 'when domain is banned' do
        let!(:domain_block) { Fabricate(:domain_block, domain: 'ap.example.com', severity: :suspend) }

        it 'does not return an account' do
          expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
        end

        it 'does not make a webfinger query' do
          subject.call('foo@ap.example.com', skip_webfinger: true)
          expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
        end
      end

      context 'when domain is not banned' do
        it 'returns the expected account' do
          expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to eq remote_account
        end

        it 'does not make a webfinger query' do
          subject.call('foo@ap.example.com', skip_webfinger: true)
          expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
        end
      end
    end

    context 'when account is not known' do
      it 'does not return an account' do
        expect(subject.call('foo@ap.example.com', skip_webfinger: true)).to be_nil
      end

      it 'does not make a webfinger query' do
        subject.call('foo@ap.example.com', skip_webfinger: true)
        expect(a_request(:get, 'https://ap.example.com/.well-known/webfinger?resource=acct:foo@ap.example.com')).to_not have_been_made
      end
    end
  end

  context 'when there is an LRDD endpoint but no resolvable account' do
    before do
      stub_request(:get, 'https://quitter.no/.well-known/host-meta').to_return(request_fixture('.host-meta.txt'))
      stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:catsrgr8@quitter.no').to_return(status: 404)
    end

    it 'returns nil' do
      expect(subject.call('catsrgr8@quitter.no')).to be_nil
    end
  end

  context 'when there is no LRDD endpoint nor resolvable account' do
    before do
      stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:catsrgr8@example.com').to_return(status: 404)
    end

    it 'returns nil' do
      expect(subject.call('catsrgr8@example.com')).to be_nil
    end
  end

  context 'when webfinger returns http gone' do
    context 'with a previously known account' do
      before do
        Fabricate(:account, username: 'hoge', domain: 'example.com', last_webfingered_at: nil)
        allow(AccountDeletionWorker).to receive(:perform_async)
      end

      it 'returns nil' do
        expect(subject.call('hoge@example.com')).to be_nil
      end

      it 'queues account deletion worker' do
        subject.call('hoge@example.com')
        expect(AccountDeletionWorker).to have_received(:perform_async)
      end
    end

    context 'with a previously unknown account' do
      it 'returns nil' do
        expect(subject.call('hoge@example.com')).to be_nil
      end
    end
  end

  context 'with a legitimate webfinger redirection' do
    before do
      webfinger = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
      stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
    end

    it 'returns new remote account' do
      account = subject.call('Foo@redirected.example.com')

      expect(account.activitypub?).to be true
      expect(account.acct).to eq 'foo@ap.example.com'
      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
    end
  end

  context 'with a misconfigured redirection' do
    before do
      webfinger = { subject: 'acct:Foo@redirected.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
      stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
    end

    it 'returns new remote account' do
      account = subject.call('Foo@redirected.example.com')

      expect(account.activitypub?).to be true
      expect(account.acct).to eq 'foo@ap.example.com'
      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
    end
  end

  context 'with too many webfinger redirections' do
    before do
      webfinger = { subject: 'acct:foo@evil.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
      stub_request(:get, 'https://redirected.example.com/.well-known/webfinger?resource=acct:Foo@redirected.example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      webfinger2 = { subject: 'acct:foo@ap.example.com', links: [{ rel: 'self', href: 'https://ap.example.com/users/foo', type: 'application/activity+json' }] }
      stub_request(:get, 'https://evil.example.com/.well-known/webfinger?resource=acct:foo@evil.example.com').to_return(body: Oj.dump(webfinger2), headers: { 'Content-Type': 'application/jrd+json' })
    end

    it 'does not return a new remote account' do
      expect(subject.call('Foo@redirected.example.com')).to be_nil
    end
  end

  context 'with webfinger response subject missing a host value' do
    let(:body) { Oj.dump({ subject: 'user@' }) }
    let(:url) { 'https://host.example/.well-known/webfinger?resource=acct:user@host.example' }

    before do
      stub_request(:get, url).to_return(status: 200, body: body)
    end

    it 'returns nil with incomplete subject in response' do
      expect(subject.call('user@host.example')).to be_nil
    end
  end

  context 'with an ActivityPub account' do
    it 'returns new remote account' do
      account = subject.call('foo@ap.example.com')

      expect(account.activitypub?).to be true
      expect(account.domain).to eq 'ap.example.com'
      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
    end

    context 'with multiple types' do
      before do
        stub_request(:get, 'https://ap.example.com/users/foo').to_return(request_fixture('activitypub-actor-individual.txt'))
      end

      it 'returns new remote account' do
        account = subject.call('foo@ap.example.com')

        expect(account.activitypub?).to be true
        expect(account.domain).to eq 'ap.example.com'
        expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
        expect(account.actor_type).to eq 'Person'
      end
    end
  end

  context 'with an already-known actor changing acct: URI' do
    let!(:duplicate) { Fabricate(:account, username: 'foo', domain: 'old.example.com', uri: 'https://ap.example.com/users/foo') }
    let!(:status)    { Fabricate(:status, account: duplicate, text: 'foo') }

    it 'returns new remote account' do
      account = subject.call('foo@ap.example.com')

      expect(account.activitypub?).to be true
      expect(account.domain).to eq 'ap.example.com'
      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
      expect(account.uri).to eq 'https://ap.example.com/users/foo'
    end

    it 'merges accounts' do
      account = subject.call('foo@ap.example.com')

      expect(status.reload.account_id).to eq account.id
      expect(Account.where(uri: account.uri).count).to eq 1
    end
  end

  context 'with an already-known acct: URI changing ActivityPub id' do
    let!(:old_account) { Fabricate(:account, username: 'foo', domain: 'ap.example.com', uri: 'https://old.example.com/users/foo', last_webfingered_at: nil) }
    let!(:status) { Fabricate(:status, account: old_account, text: 'foo') }

    it 'returns new remote account' do
      account = subject.call('foo@ap.example.com')

      expect(account.activitypub?).to be true
      expect(account.domain).to eq 'ap.example.com'
      expect(account.inbox_url).to eq 'https://ap.example.com/users/foo/inbox'
      expect(account.uri).to eq 'https://ap.example.com/users/foo'
    end
  end

  it 'processes one remote account at a time using locks' do
    wait_for_start = true
    fail_occurred  = false
    return_values  = Concurrent::Array.new

    threads = Array.new(5) do
      Thread.new do
        true while wait_for_start

        begin
          return_values << described_class.new.call('foo@ap.example.com')
        rescue ActiveRecord::RecordNotUnique
          fail_occurred = true
        ensure
          RedisConfiguration.pool.checkin if Thread.current[:redis]
        end
      end
    end

    wait_for_start = false
    threads.each(&:join)

    expect(fail_occurred).to be false
    expect(return_values).to_not include(nil)
  end
end


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

class FollowService < BaseService
  include Redisable
  include Payloadable
  include DomainControlHelper

  # Follow a remote user, notify remote user about the follow
  # @param [Account] source_account From which to follow
  # @param [Account] target_account Account to follow
  # @param [Hash] options
  # @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
  # @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
  # @option [Array<String>] :languages Which languages to allow on the home feed from this account, defaults to all
  # @option [Boolean] :bypass_locked
  # @option [Boolean] :bypass_limit Allow following past the total follow number
  # @option [Boolean] :with_rate_limit
  def call(source_account, target_account, options = {})
    @source_account = source_account
    @target_account = target_account
    @options        = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)

    raise ActiveRecord::RecordNotFound if following_not_possible?
    raise Mastodon::NotPermittedError  if following_not_allowed?

    if @source_account.following?(@target_account)
      return change_follow_options!
    elsif @source_account.requested?(@target_account)
      return change_follow_request_options!
    end

    ActivityTracker.increment('activity:interactions')

    # When an account follows someone for the first time, avoid showing
    # an empty home feed while the follow request is being processed
    # and the feeds are being merged
    mark_home_feed_as_partial! if @source_account.not_following_anyone?

    if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub?
      request_follow!
    elsif @target_account.local?
      direct_follow!
    end
  end

  private

  def mark_home_feed_as_partial!
    redis.set("account:#{@source_account.id}:regeneration", true, nx: true, ex: 1.day.seconds)
  end

  def following_not_possible?
    @target_account.nil? || @target_account.id == @source_account.id || @target_account.unavailable?
  end

  def following_not_allowed?
    domain_not_allowed?(@target_account.domain) || @target_account.blocking?(@source_account) || @source_account.blocking?(@target_account) || @target_account.moved? || (!@target_account.local? && @target_account.ostatus?) || @source_account.domain_blocking?(@target_account.domain)
  end

  def change_follow_options!
    @source_account.follow!(@target_account, **follow_options)
  end

  def change_follow_request_options!
    @source_account.request_follow!(@target_account, **follow_options)
  end

  def request_follow!
    follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))

    if @target_account.local?
      LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
    elsif @target_account.activitypub?
      ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
    end

    follow_request
  end

  def direct_follow!
    follow = @source_account.follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))

    LocalNotificationWorker.perform_async(@target_account.id, follow.id, follow.class.name, 'follow')
    MergeWorker.perform_async(@target_account.id, @source_account.id)

    follow
  end

  def build_json(follow_request)
    Oj.dump(serialize_payload(follow_request, ActivityPub::FollowSerializer))
  end

  def follow_options
    @options.slice(:reblogs, :notify, :languages)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe FollowService, type: :service do
  subject { described_class.new }

  let(:sender) { Fabricate(:account, username: 'alice') }

  context 'when local account' do
    describe 'locked account' do
      let(:bob) { Fabricate(:account, locked: true, username: 'bob') }

      before do
        subject.call(sender, bob)
      end

      it 'creates a follow request with reblogs' do
        expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil
      end
    end

    describe 'locked account, no reblogs' do
      let(:bob) { Fabricate(:account, locked: true, username: 'bob') }

      before do
        subject.call(sender, bob, reblogs: false)
      end

      it 'creates a follow request without reblogs' do
        expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil
      end
    end

    describe 'unlocked account, from silenced account' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        sender.touch(:silenced_at)
        subject.call(sender, bob)
      end

      it 'creates a follow request with reblogs' do
        expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil
      end
    end

    describe 'unlocked account, from a muted account' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        bob.mute!(sender)
        subject.call(sender, bob)
      end

      it 'creates a following relation with reblogs' do
        expect(sender.following?(bob)).to be true
        expect(sender.muting_reblogs?(bob)).to be false
      end
    end

    describe 'unlocked account' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        subject.call(sender, bob)
      end

      it 'creates a following relation with reblogs' do
        expect(sender.following?(bob)).to be true
        expect(sender.muting_reblogs?(bob)).to be false
      end
    end

    describe 'unlocked account, no reblogs' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        subject.call(sender, bob, reblogs: false)
      end

      it 'creates a following relation without reblogs' do
        expect(sender.following?(bob)).to be true
        expect(sender.muting_reblogs?(bob)).to be true
      end
    end

    describe 'already followed account' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        sender.follow!(bob)
        subject.call(sender, bob)
      end

      it 'keeps a following relation' do
        expect(sender.following?(bob)).to be true
      end
    end

    describe 'already followed account, turning reblogs off' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        sender.follow!(bob, reblogs: true)
        subject.call(sender, bob, reblogs: false)
      end

      it 'disables reblogs' do
        expect(sender.muting_reblogs?(bob)).to be true
      end
    end

    describe 'already followed account, turning reblogs on' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        sender.follow!(bob, reblogs: false)
        subject.call(sender, bob, reblogs: true)
      end

      it 'disables reblogs' do
        expect(sender.muting_reblogs?(bob)).to be false
      end
    end

    describe 'already followed account, changing languages' do
      let(:bob) { Fabricate(:account, username: 'bob') }

      before do
        sender.follow!(bob)
        subject.call(sender, bob, languages: %w(en es))
      end

      it 'changes languages' do
        expect(Follow.find_by(account: sender, target_account: bob)&.languages).to match_array %w(en es)
      end
    end
  end

  context 'when remote ActivityPub account' do
    let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }

    before do
      stub_request(:post, 'http://example.com/inbox').to_return(status: 200, body: '', headers: {})
      subject.call(sender, bob)
    end

    it 'creates follow request' do
      expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil
    end

    it 'sends a follow activity to the inbox' do
      expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
    end
  end
end


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

class DeleteAccountService < BaseService
  include Payloadable

  ASSOCIATIONS_ON_SUSPEND = %w(
    account_notes
    account_pins
    active_relationships
    aliases
    block_relationships
    blocked_by_relationships
    conversation_mutes
    conversations
    custom_filters
    devices
    domain_blocks
    featured_tags
    follow_requests
    list_accounts
    migrations
    mute_relationships
    muted_by_relationships
    notifications
    owned_lists
    passive_relationships
    report_notes
    scheduled_statuses
    status_pins
  ).freeze

  # The following associations have no important side-effects
  # in callbacks and all of their own associations are secured
  # by foreign keys, making them safe to delete without loading
  # into memory
  ASSOCIATIONS_WITHOUT_SIDE_EFFECTS = %w(
    account_notes
    account_pins
    aliases
    conversation_mutes
    conversations
    custom_filters
    devices
    domain_blocks
    featured_tags
    follow_requests
    list_accounts
    migrations
    mute_relationships
    muted_by_relationships
    notifications
    owned_lists
    scheduled_statuses
    status_pins
  )

  ASSOCIATIONS_ON_DESTROY = %w(
    reports
    targeted_moderation_notes
    targeted_reports
  ).freeze

  # Suspend or remove an account and remove as much of its data
  # as possible. If it's a local account and it has not been confirmed
  # or never been approved, then side effects are skipped and both
  # the user and account records are removed fully. Otherwise,
  # it is controlled by options.
  # @param [Account]
  # @param [Hash] options
  # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
  # @option [Boolean] :reserve_username Keep account record
  # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
  # @option [Boolean] :skip_activitypub Skip sending ActivityPub payloads. Implied by :skip_side_effects
  # @option [Time]    :suspended_at Only applicable when :reserve_username is true
  def call(account, **options)
    @account = account
    @options = { reserve_username: true, reserve_email: true }.merge(options)

    if @account.local? && @account.user_unconfirmed_or_pending?
      @options[:reserve_email]     = false
      @options[:reserve_username]  = false
      @options[:skip_side_effects] = true
    end

    @options[:skip_activitypub] = true if @options[:skip_side_effects]

    distribute_activities!
    purge_content!
    fulfill_deletion_request!
  end

  private

  def distribute_activities!
    return if skip_activitypub?

    if @account.local?
      delete_actor!
    elsif @account.activitypub?
      reject_follows!
      undo_follows!
    end
  end

  def reject_follows!
    # When deleting a remote account, the account obviously doesn't
    # actually become deleted on its origin server, i.e. unlike a
    # locally deleted account it continues to have access to its home
    # feed and other content. To prevent it from being able to continue
    # to access toots it would receive because it follows local accounts,
    # we have to force it to unfollow them.

    ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
      [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
    end
  end

  def undo_follows!
    # When deleting a remote account, the account obviously doesn't
    # actually become deleted on its origin server, but following relationships
    # are severed on our end. Therefore, make the remote server aware that the
    # follow relationships are severed to avoid confusion and potential issues
    # if the remote account gets un-suspended.

    ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
      [Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url]
    end
  end

  def purge_user!
    return if !@account.local? || @account.user.nil?

    if keep_user_record?
      @account.user.disable!
      @account.user.invites.where(uses: 0).destroy_all
    else
      @account.user.destroy
    end
  end

  def purge_content!
    purge_user!
    purge_profile!
    purge_statuses!
    purge_mentions!
    purge_media_attachments!
    purge_polls!
    purge_generated_notifications!
    purge_favourites!
    purge_bookmarks!
    purge_feeds!
    purge_other_associations!

    @account.destroy unless keep_account_record?
  end

  def purge_statuses!
    @account.statuses.reorder(nil).where.not(id: reported_status_ids).in_batches do |statuses|
      BatchedRemoveStatusService.new.call(statuses, skip_side_effects: skip_side_effects?)
    end
  end

  def purge_mentions!
    @account.mentions.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
  end

  def purge_media_attachments!
    @account.media_attachments.find_each do |media_attachment|
      next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)

      media_attachment.destroy
    end
  end

  def purge_polls!
    @account.polls.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
  end

  def purge_generated_notifications!
    # By deleting polls and statuses without callbacks, we've left behind
    # polymorphically associated notifications generated by this account

    Notification.where(from_account: @account).in_batches.delete_all
  end

  def purge_favourites!
    @account.favourites.in_batches do |favourites|
      ids = favourites.pluck(:status_id)
      StatusStat.where(status_id: ids).update_all('favourites_count = GREATEST(0, favourites_count - 1)')
      Chewy.strategy.current.update(StatusesIndex, ids) if Chewy.enabled?
      Rails.cache.delete_multi(ids.map { |id| "statuses/#{id}" })
      favourites.delete_all
    end
  end

  def purge_bookmarks!
    @account.bookmarks.in_batches do |bookmarks|
      Chewy.strategy.current.update(StatusesIndex, bookmarks.pluck(:status_id)) if Chewy.enabled?
      bookmarks.delete_all
    end
  end

  def purge_other_associations!
    associations_for_destruction.each do |association_name|
      purge_association(association_name)
    end
  end

  def purge_feeds!
    return unless @account.local?

    FeedManager.instance.clean_feeds!(:home, [@account.id])
    FeedManager.instance.clean_feeds!(:list, @account.owned_lists.pluck(:id))
  end

  def purge_profile!
    # If the account is going to be destroyed
    # there is no point wasting time updating
    # its values first

    return unless keep_account_record?

    @account.silenced_at         = nil
    @account.suspended_at        = @options[:suspended_at] || Time.now.utc
    @account.suspension_origin   = :local
    @account.locked              = false
    @account.memorial            = false
    @account.discoverable        = false
    @account.trendable           = false
    @account.display_name        = ''
    @account.note                = ''
    @account.fields              = []
    @account.statuses_count      = 0
    @account.followers_count     = 0
    @account.following_count     = 0
    @account.moved_to_account    = nil
    @account.reviewed_at         = nil
    @account.requested_review_at = nil
    @account.also_known_as       = []
    @account.avatar.destroy
    @account.header.destroy
    @account.save!
  end

  def fulfill_deletion_request!
    @account.deletion_request&.destroy
  end

  def purge_association(association_name)
    association = @account.public_send(association_name)

    if ASSOCIATIONS_WITHOUT_SIDE_EFFECTS.include?(association_name)
      association.in_batches.delete_all
    else
      association.in_batches.destroy_all
    end
  end

  def delete_actor!
    ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes, limit: 1_000) do |inbox_url|
      [delete_actor_json, @account.id, inbox_url]
    end

    ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes, limit: 1_000) do |inbox_url|
      [delete_actor_json, @account.id, inbox_url]
    end
  end

  def delete_actor_json
    @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account, always_sign: true))
  end

  def delivery_inboxes
    @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
  end

  def low_priority_delivery_inboxes
    Account.inboxes - delivery_inboxes
  end

  def reported_status_ids
    @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
  end

  def associations_for_destruction
    if keep_account_record?
      ASSOCIATIONS_ON_SUSPEND
    else
      ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
    end
  end

  def keep_user_record?
    @options[:reserve_email]
  end

  def keep_account_record?
    @options[:reserve_username]
  end

  def skip_side_effects?
    @options[:skip_side_effects]
  end

  def skip_activitypub?
    @options[:skip_activitypub]
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe DeleteAccountService, type: :service do
  shared_examples 'common behavior' do
    subject { described_class.new.call(account) }

    let!(:status) { Fabricate(:status, account: account) }
    let!(:mention) { Fabricate(:mention, account: local_follower) }
    let!(:status_with_mention) { Fabricate(:status, account: account, mentions: [mention]) }
    let!(:media_attachment) { Fabricate(:media_attachment, account: account) }
    let!(:notification) { Fabricate(:notification, account: account) }
    let!(:favourite) { Fabricate(:favourite, account: account, status: Fabricate(:status, account: local_follower)) }
    let!(:poll) { Fabricate(:poll, account: account) }
    let!(:poll_vote) { Fabricate(:poll_vote, account: local_follower, poll: poll) }

    let!(:active_relationship) { Fabricate(:follow, account: account, target_account: local_follower) }
    let!(:passive_relationship) { Fabricate(:follow, account: local_follower, target_account: account) }
    let!(:endorsement) { Fabricate(:account_pin, account: local_follower, target_account: account) }

    let!(:mention_notification) { Fabricate(:notification, account: local_follower, activity: mention, type: :mention) }
    let!(:status_notification) { Fabricate(:notification, account: local_follower, activity: status, type: :status) }
    let!(:poll_notification) { Fabricate(:notification, account: local_follower, activity: poll, type: :poll) }
    let!(:favourite_notification) { Fabricate(:notification, account: local_follower, activity: favourite, type: :favourite) }
    let!(:follow_notification) { Fabricate(:notification, account: local_follower, activity: active_relationship, type: :follow) }

    let!(:account_note) { Fabricate(:account_note, account: account) }

    it 'deletes associated owned records' do
      expect { subject }.to change {
        [
          account.statuses,
          account.media_attachments,
          account.notifications,
          account.favourites,
          account.active_relationships,
          account.passive_relationships,
          account.polls,
          account.account_notes,
        ].map(&:count)
      }.from([2, 1, 1, 1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0, 0, 0, 0])
    end

    it 'deletes associated target records' do
      expect { subject }.to change {
        [
          AccountPin.where(target_account: account),
        ].map(&:count)
      }.from([1]).to([0])
    end

    it 'deletes associated target notifications' do
      expect { subject }.to change {
        %w(
          poll favourite status mention follow
        ).map { |type| Notification.where(type: type).count }
      }.from([1, 1, 1, 1, 1]).to([0, 0, 0, 0, 0])
    end
  end

  describe '#call on local account' do
    before do
      stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
      stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
    end

    let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', domain: 'alice.com', protocol: :activitypub) }
    let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', domain: 'bob.com', protocol: :activitypub) }

    include_examples 'common behavior' do
      let!(:account) { Fabricate(:account) }
      let!(:local_follower) { Fabricate(:account) }

      it 'sends a delete actor activity to all known inboxes' do
        subject
        expect(a_request(:post, 'https://alice.com/inbox')).to have_been_made.once
        expect(a_request(:post, 'https://bob.com/inbox')).to have_been_made.once
      end
    end
  end

  describe '#call on remote account' do
    before do
      stub_request(:post, 'https://alice.com/inbox').to_return(status: 201)
      stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
    end

    include_examples 'common behavior' do
      let!(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
      let!(:local_follower) { Fabricate(:account) }

      it 'sends expected activities to followed and follower inboxes' do
        subject

        expect(a_request(:post, account.inbox_url).with(
                 body:
                   hash_including({
                     'type' => 'Reject',
                     'object' => hash_including({
                       'type' => 'Follow',
                       'actor' => account.uri,
                       'object' => ActivityPub::TagManager.instance.uri_for(local_follower),
                     }),
                   })
               )).to have_been_made.once

        expect(a_request(:post, account.inbox_url).with(
                 body: hash_including({
                   'type' => 'Undo',
                   'object' => hash_including({
                     'type' => 'Follow',
                     'actor' => ActivityPub::TagManager.instance.uri_for(local_follower),
                     'object' => account.uri,
                   }),
                 })
               )).to have_been_made.once
      end
    end
  end
end


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

class RemoveStatusService < BaseService
  include Redisable
  include Payloadable
  include Lockable

  # Delete a status
  # @param   [Status] status
  # @param   [Hash] options
  # @option  [Boolean] :redraft
  # @option  [Boolean] :immediate
  # @option  [Boolean] :preserve
  # @option  [Boolean] :original_removed
  # @option  [Boolean] :skip_streaming
  def call(status, **options)
    @payload  = Oj.dump(event: :delete, payload: status.id.to_s)
    @status   = status
    @account  = status.account
    @options  = options

    with_redis_lock("distribute:#{@status.id}") do
      @status.discard_with_reblogs

      StatusPin.find_by(status: @status)&.destroy

      remove_from_self if @account.local?
      remove_from_followers
      remove_from_lists

      # There is no reason to send out Undo activities when the
      # cause is that the original object has been removed, since
      # original object being removed implicitly removes reblogs
      # of it. The Delete activity of the original is forwarded
      # separately.
      remove_from_remote_reach if @account.local? && !@options[:original_removed]

      # Since reblogs don't mention anyone, don't get reblogged,
      # favourited and don't contain their own media attachments
      # or hashtags, this can be skipped
      unless @status.reblog?
        remove_from_mentions
        remove_reblogs
        remove_from_hashtags
        remove_from_public
        remove_from_media if @status.with_media?
        remove_media
      end

      @status.destroy! if permanently?
    end
  end

  private

  # The following FeedManager calls all do not result in redis publishes for
  # streaming, as the `:update` option is false

  def remove_from_self
    FeedManager.instance.unpush_from_home(@account, @status)
  end

  def remove_from_followers
    @account.followers_for_local_distribution.includes(:user).reorder(nil).find_each do |follower|
      FeedManager.instance.unpush_from_home(follower, @status)
    end
  end

  def remove_from_lists
    @account.lists_for_local_distribution.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |list|
      FeedManager.instance.unpush_from_list(list, @status)
    end
  end

  def remove_from_mentions
    # For limited visibility statuses, the mentions that determine
    # who receives them in their home feed are a subset of followers
    # and therefore the delete is already handled by sending it to all
    # followers. Here we send a delete to actively mentioned accounts
    # that may not follow the account

    return if skip_streaming?

    @status.active_mentions.find_each do |mention|
      redis.publish("timeline:#{mention.account_id}", @payload)
    end
  end

  def remove_from_remote_reach
    # Followers, relays, people who got mentioned in the status,
    # or who reblogged it from someone else might not follow
    # the author and wouldn't normally receive the delete
    # notification - so here, we explicitly send it to them

    status_reach_finder = StatusReachFinder.new(@status, unsafe: true)

    ActivityPub::DeliveryWorker.push_bulk(status_reach_finder.inboxes, limit: 1_000) do |inbox_url|
      [signed_activity_json, @account.id, inbox_url]
    end
  end

  def signed_activity_json
    @signed_activity_json ||= Oj.dump(serialize_payload(@status, @status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer, signer: @account, always_sign: true))
  end

  def remove_reblogs
    # We delete reblogs of the status before the original status,
    # because once original status is gone, reblogs will disappear
    # without us being able to do all the fancy stuff

    @status.reblogs.rewhere(deleted_at: [nil, @status.deleted_at]).includes(:account).reorder(nil).find_each do |reblog|
      RemoveStatusService.new.call(reblog, original_removed: true, skip_streaming: skip_streaming?)
    end
  end

  def remove_from_hashtags
    @account.featured_tags.where(tag_id: @status.tags.map(&:id)).find_each do |featured_tag|
      featured_tag.decrement(@status.id)
    end

    return unless @status.public_visibility?

    return if skip_streaming?

    @status.tags.map(&:name).each do |hashtag|
      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}", @payload)
      redis.publish("timeline:hashtag:#{hashtag.mb_chars.downcase}:local", @payload) if @status.local?
    end
  end

  def remove_from_public
    return unless @status.public_visibility?

    return if skip_streaming?

    redis.publish('timeline:public', @payload)
    redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', @payload)
  end

  def remove_from_media
    return unless @status.public_visibility?

    return if skip_streaming?

    redis.publish('timeline:public:media', @payload)
    redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', @payload)
  end

  def remove_media
    return if @options[:redraft] || !permanently?

    @status.media_attachments.destroy_all
  end

  def permanently?
    @options[:immediate] || !(@options[:preserve] || @status.reported?)
  end

  def skip_streaming?
    !!@options[:skip_streaming]
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe RemoveStatusService, type: :service do
  subject { described_class.new }

  let!(:alice)  { Fabricate(:account) }
  let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com') }
  let!(:jeff)   { Fabricate(:account) }
  let!(:hank)   { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
  let!(:bill)   { Fabricate(:account, username: 'bill', protocol: :activitypub, domain: 'example2.com', inbox_url: 'http://example2.com/inbox') }

  before do
    stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
    stub_request(:post, 'http://example2.com/inbox').to_return(status: 200)

    jeff.follow!(alice)
    hank.follow!(alice)
  end

  context 'when removed status is not a reblog' do
    let!(:status) { PostStatusService.new.call(alice, text: 'Hello @bob@example.com ThisIsASecret') }

    before do
      FavouriteService.new.call(jeff, status)
      Fabricate(:status, account: bill, reblog: status, uri: 'hoge')
    end

    it 'removes status from author\'s home feed' do
      subject.call(status)
      expect(HomeFeed.new(alice).get(10).pluck(:id)).to_not include(status.id)
    end

    it 'removes status from local follower\'s home feed' do
      subject.call(status)
      expect(HomeFeed.new(jeff).get(10).pluck(:id)).to_not include(status.id)
    end

    it 'sends Delete activity to followers' do
      subject.call(status)
      expect(a_request(:post, 'http://example.com/inbox').with(
               body: hash_including({
                 'type' => 'Delete',
                 'object' => {
                   'type' => 'Tombstone',
                   'id' => ActivityPub::TagManager.instance.uri_for(status),
                   'atomUri' => OStatus::TagManager.instance.uri_for(status),
                 },
               })
             )).to have_been_made.once
    end

    it 'sends Delete activity to rebloggers' do
      subject.call(status)
      expect(a_request(:post, 'http://example2.com/inbox').with(
               body: hash_including({
                 'type' => 'Delete',
                 'object' => {
                   'type' => 'Tombstone',
                   'id' => ActivityPub::TagManager.instance.uri_for(status),
                   'atomUri' => OStatus::TagManager.instance.uri_for(status),
                 },
               })
             )).to have_been_made.once
    end

    it 'remove status from notifications' do
      expect { subject.call(status) }.to change {
        Notification.where(activity_type: 'Favourite', from_account: jeff, account: alice).count
      }.from(1).to(0)
    end
  end

  context 'when removed status is a private self-reblog' do
    let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :private) }
    let!(:status) { ReblogService.new.call(alice, original_status) }

    it 'sends Undo activity to followers' do
      subject.call(status)
      expect(a_request(:post, 'http://example.com/inbox').with(
               body: hash_including({
                 'type' => 'Undo',
                 'object' => hash_including({
                   'type' => 'Announce',
                   'object' => ActivityPub::TagManager.instance.uri_for(original_status),
                 }),
               })
             )).to have_been_made.once
    end
  end

  context 'when removed status is public self-reblog' do
    let!(:original_status) { Fabricate(:status, account: alice, text: 'Hello ThisIsASecret', visibility: :public) }
    let!(:status) { ReblogService.new.call(alice, original_status) }

    it 'sends Undo activity to followers' do
      subject.call(status)
      expect(a_request(:post, 'http://example.com/inbox').with(
               body: hash_including({
                 'type' => 'Undo',
                 'object' => hash_including({
                   'type' => 'Announce',
                   'object' => ActivityPub::TagManager.instance.uri_for(original_status),
                 }),
               })
             )).to have_been_made.once
    end
  end
end


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

class BulkImportRowService
  def call(row)
    @account = row.bulk_import.account
    @data    = row.data
    @type    = row.bulk_import.type.to_sym

    case @type
    when :following, :blocking, :muting, :lists
      target_acct     = @data['acct']
      target_domain   = domain(target_acct)
      @target_account = stoplight_wrap_request(target_domain) { ResolveAccountService.new.call(target_acct, { check_delivery_availability: true }) }
      return false if @target_account.nil?
    when :bookmarks
      target_uri      = @data['uri']
      target_domain   = Addressable::URI.parse(target_uri).normalized_host
      @target_status = ActivityPub::TagManager.instance.uri_to_resource(target_uri, Status)
      return false if @target_status.nil? && ActivityPub::TagManager.instance.local_uri?(target_uri)

      @target_status ||= stoplight_wrap_request(target_domain) { ActivityPub::FetchRemoteStatusService.new.call(target_uri) }
      return false if @target_status.nil?
    end

    case @type
    when :following
      FollowService.new.call(@account, @target_account, reblogs: @data['show_reblogs'], notify: @data['notify'], languages: @data['languages'])
    when :blocking
      BlockService.new.call(@account, @target_account)
    when :muting
      MuteService.new.call(@account, @target_account, notifications: @data['hide_notifications'])
    when :bookmarks
      return false unless StatusPolicy.new(@account, @target_status).show?

      @account.bookmarks.find_or_create_by!(status: @target_status)
    when :lists
      list = @account.owned_lists.find_or_create_by!(title: @data['list_name'])

      FollowService.new.call(@account, @target_account) unless @account.id == @target_account.id

      list.accounts << @target_account
    end

    true
  rescue ActiveRecord::RecordNotFound
    false
  end

  def domain(uri)
    domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
    TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
  end

  def stoplight_wrap_request(domain, &block)
    if domain.present?
      Stoplight("source:#{domain}", &block)
        .with_fallback { nil }
        .with_threshold(1)
        .with_cool_off_time(5.minutes.seconds)
        .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
        .run
    else
      yield
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BulkImportRowService do
  subject { described_class.new }

  let(:account)    { Fabricate(:account) }
  let(:import)     { Fabricate(:bulk_import, account: account, type: import_type) }
  let(:import_row) { Fabricate(:bulk_import_row, bulk_import: import, data: data) }

  describe '#call' do
    context 'when importing a follow' do
      let(:import_type)    { 'following' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(FollowService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(FollowService).to receive(:new).and_return(service_double)
      end

      it 'calls FollowService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account, { reblogs: nil, notify: nil, languages: nil })
      end
    end

    context 'when importing a block' do
      let(:import_type)    { 'blocking' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(BlockService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(BlockService).to receive(:new).and_return(service_double)
      end

      it 'calls BlockService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account)
      end
    end

    context 'when importing a mute' do
      let(:import_type)    { 'muting' }
      let(:target_account) { Fabricate(:account) }
      let(:service_double) { instance_double(MuteService, call: nil) }
      let(:data) do
        { 'acct' => target_account.acct }
      end

      before do
        allow(MuteService).to receive(:new).and_return(service_double)
      end

      it 'calls MuteService with the expected arguments and returns true' do
        expect(subject.call(import_row)).to be true

        expect(service_double).to have_received(:call).with(account, target_account, { notifications: nil })
      end
    end

    context 'when importing a bookmark' do
      let(:import_type) { 'bookmarks' }
      let(:data) do
        { 'uri' => ActivityPub::TagManager.instance.uri_for(target_status) }
      end

      context 'when the status is public' do
        let(:target_status) { Fabricate(:status) }

        it 'bookmarks the status and returns true' do
          expect(subject.call(import_row)).to be true
          expect(account.bookmarked?(target_status)).to be true
        end
      end

      context 'when the status is not accessible to the user' do
        let(:target_status) { Fabricate(:status, visibility: :direct) }

        it 'does not bookmark the status and returns false' do
          expect(subject.call(import_row)).to be false
          expect(account.bookmarked?(target_status)).to be false
        end
      end
    end

    context 'when importing a list row' do
      let(:import_type) { 'lists' }
      let(:target_account) { Fabricate(:account) }
      let(:data) do
        { 'acct' => target_account.acct, 'list_name' => 'my list' }
      end

      shared_examples 'common behavior' do
        context 'when the target account is already followed' do
          before do
            account.follow!(target_account)
          end

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the user already requested to follow the target account' do
          before do
            account.request_follow!(target_account)
          end

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the target account is neither followed nor requested' do
          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end

        context 'when the target account is the user themself' do
          let(:target_account) { account }

          it 'returns true' do
            expect(subject.call(import_row)).to be true
          end

          it 'adds the target account to the list' do
            expect { subject.call(import_row) }.to change { ListAccount.joins(:list).exists?(account_id: target_account.id, list: { title: 'my list' }) }.from(false).to(true)
          end
        end
      end

      context 'when the list does not exist yet' do
        include_examples 'common behavior'
      end

      context 'when the list exists' do
        before do
          Fabricate(:list, account: account, title: 'my list')
        end

        include_examples 'common behavior'

        it 'does not create a new list' do
          account.follow!(target_account)

          expect { subject.call(import_row) }.to_not(change { List.where(title: 'my list').count })
        end
      end
    end
  end
end


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

class ActivityPub::FetchRemoteKeyService < BaseService
  include JsonLdHelper

  class Error < StandardError; end

  # Returns actor that owns the key
  def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
    raise Error, 'No key URI given' if uri.blank?

    if prefetched_body.nil?
      if id
        @json = fetch_resource_without_id_validation(uri)
        if actor_type?
          @json = fetch_resource(@json['id'], true)
        elsif uri != @json['id']
          raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
        end
      else
        @json = fetch_resource(uri, id)
      end
    else
      @json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
    end

    raise Error, "Unable to fetch key JSON at #{uri}" if @json.nil?
    raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?(@json)
    raise Error, "Unexpected object type for key #{uri}" unless expected_type?
    return find_actor(@json['id'], @json, suppress_errors) if actor_type?

    @owner = fetch_resource(owner_uri, true)

    raise Error, "Unable to fetch actor JSON #{owner_uri}" if @owner.nil?
    raise Error, "Unsupported JSON-LD context for document #{owner_uri}" unless supported_context?(@owner)
    raise Error, "Unexpected object type for actor #{owner_uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_owner_type?
    raise Error, "publicKey id for #{owner_uri} does not correspond to #{@json['id']}" unless confirmed_owner?

    find_actor(owner_uri, @owner, suppress_errors)
  rescue Error => e
    Rails.logger.debug { "Fetching key #{uri} failed: #{e.message}" }
    raise unless suppress_errors
  end

  private

  def find_actor(uri, prefetched_body, suppress_errors)
    actor   = ActivityPub::TagManager.instance.uri_to_actor(uri)
    actor ||= ActivityPub::FetchRemoteActorService.new.call(uri, prefetched_body: prefetched_body, suppress_errors: suppress_errors)
    actor
  end

  def expected_type?
    actor_type? || public_key?
  end

  def actor_type?
    equals_or_includes_any?(@json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
  end

  def public_key?
    @json['publicKeyPem'].present? && @json['owner'].present?
  end

  def owner_uri
    @owner_uri ||= value_or_id(@json['owner'])
  end

  def expected_owner_type?
    equals_or_includes_any?(@owner['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
  end

  def confirmed_owner?
    value_or_id(@owner['publicKey']) == @json['id']
  end
end

```


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

class ActivityPub::ProcessCollectionService < BaseService
  include JsonLdHelper

  def call(body, actor, **options)
    @account = actor
    @json    = original_json = Oj.load(body, mode: :strict)
    @options = options

    return unless @json.is_a?(Hash)

    begin
      @json = compact(@json) if @json['signature'].is_a?(Hash)
    rescue JSON::LD::JsonLdError => e
      Rails.logger.debug { "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}" }
      @json = original_json.without('signature')
    end

    return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
    return unless @account.is_a?(Account)

    if @json['signature'].present?
      # We have verified the signature, but in the compaction step above, might
      # have introduced incompatibilities with other servers that do not
      # normalize the JSON-LD documents (for instance, previous Mastodon
      # versions), so skip redistribution if we can't get a safe document.
      patch_for_forwarding!(original_json, @json)
      @json.delete('signature') unless safe_for_forwarding?(original_json, @json)
    end

    case @json['type']
    when 'Collection', 'CollectionPage'
      process_items @json['items']
    when 'OrderedCollection', 'OrderedCollectionPage'
      process_items @json['orderedItems']
    else
      process_items [@json]
    end
  rescue Oj::ParseError
    nil
  end

  private

  def different_actor?
    @json['actor'].present? && value_or_id(@json['actor']) != @account.uri
  end

  def suspended_actor?
    @account.suspended? && !activity_allowed_while_suspended?
  end

  def activity_allowed_while_suspended?
    %w(Delete Reject Undo Update).include?(@json['type'])
  end

  def process_items(items)
    items.reverse_each.filter_map { |item| process_item(item) }
  end

  def supported_context?
    super(@json)
  end

  def process_item(item)
    activity = ActivityPub::Activity.factory(item, @account, **@options)
    activity&.perform
  end

  def verify_account!
    @options[:relayed_through_actor] = @account
    @account = ActivityPub::LinkedDataSignature.new(@json).verify_actor!
    @account = nil unless @account.is_a?(Account)
    @account
  rescue JSON::LD::JsonLdError, RDF::WriterError => e
    Rails.logger.debug { "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}" }
    nil
  end
end

```


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

class ActivityPub::FetchRemoteActorService < BaseService
  include JsonLdHelper
  include DomainControlHelper
  include WebfingerHelper

  class Error < StandardError; end

  SUPPORTED_TYPES = %w(Application Group Organization Person Service).freeze

  # Does a WebFinger roundtrip on each call, unless `only_key` is true
  def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
    return if domain_not_allowed?(uri)
    return ActivityPub::TagManager.instance.uri_to_actor(uri) if ActivityPub::TagManager.instance.local_uri?(uri)

    @json = begin
      if prefetched_body.nil?
        fetch_resource(uri, id)
      else
        body_to_json(prefetched_body, compare_id: id ? uri : nil)
      end
    rescue Oj::ParseError
      raise Error, "Error parsing JSON-LD document #{uri}"
    end

    raise Error, "Error fetching actor JSON at #{uri}" if @json.nil?
    raise Error, "Unsupported JSON-LD context for document #{uri}" unless supported_context?
    raise Error, "Unexpected object type for actor #{uri} (expected any of: #{SUPPORTED_TYPES})" unless expected_type?
    raise Error, "Actor #{uri} has moved to #{@json['movedTo']}" if break_on_redirect && @json['movedTo'].present?
    raise Error, "Actor #{uri} has no 'preferredUsername', which is a requirement for Mastodon compatibility" if @json['preferredUsername'].blank?

    @uri      = @json['id']
    @username = @json['preferredUsername']
    @domain   = Addressable::URI.parse(@uri).normalized_host

    check_webfinger! unless only_key

    ActivityPub::ProcessAccountService.new.call(@username, @domain, @json, only_key: only_key, verified_webfinger: !only_key, request_id: request_id)
  rescue Error => e
    Rails.logger.debug { "Fetching actor #{uri} failed: #{e.message}" }
    raise unless suppress_errors
  end

  private

  def check_webfinger!
    webfinger                            = webfinger!("acct:#{@username}@#{@domain}")
    confirmed_username, confirmed_domain = split_acct(webfinger.subject)

    if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
      raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri

      return
    end

    webfinger                            = webfinger!("acct:#{confirmed_username}@#{confirmed_domain}")
    @username, @domain                   = split_acct(webfinger.subject)

    raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{@uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?

    raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
  rescue Webfinger::RedirectError => e
    raise Error, e.message
  rescue Webfinger::Error => e
    raise Error, "Webfinger error when resolving #{@username}@#{@domain}: #{e.message}"
  end

  def split_acct(acct)
    acct.delete_prefix('acct:').split('@')
  end

  def supported_context?
    super(@json)
  end

  def expected_type?
    equals_or_includes_any?(@json['type'], SUPPORTED_TYPES)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::FetchRemoteActorService, type: :service do
  subject { described_class.new }

  let!(:actor) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: 'https://example.com/alice',
      type: 'Person',
      preferredUsername: 'alice',
      name: 'Alice',
      summary: 'Foo bar',
      inbox: 'http://example.com/alice/inbox',
    }
  end

  describe '#call' do
    let(:account) { subject.call('https://example.com/alice', id: true) }

    shared_examples 'sets profile data' do
      it 'returns an account' do
        expect(account).to be_an Account
      end

      it 'sets display name' do
        expect(account.display_name).to eq 'Alice'
      end

      it 'sets note' do
        expect(account.note).to eq 'Foo bar'
      end

      it 'sets URL' do
        expect(account.url).to eq 'https://example.com/alice'
      end
    end

    context 'when the account does not have a inbox' do
      let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }

      before do
        actor[:inbox] = nil

        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      end

      it 'fetches resource' do
        account
        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
      end

      it 'looks up webfinger' do
        account
        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
      end

      it 'returns nil' do
        expect(account).to be_nil
      end
    end

    context 'when URI and WebFinger share the same host' do
      let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }

      before do
        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      end

      it 'fetches resource' do
        account
        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
      end

      it 'looks up webfinger' do
        account
        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
      end

      it 'sets username and domain from webfinger' do
        expect(account.username).to eq 'alice'
        expect(account.domain).to eq 'example.com'
      end

      include_examples 'sets profile data'
    end

    context 'when WebFinger presents different domain than URI' do
      let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/alice' }] } }

      before do
        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
        stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      end

      it 'fetches resource' do
        account
        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
      end

      it 'looks up webfinger' do
        account
        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
      end

      it 'looks up "redirected" webfinger' do
        account
        expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
      end

      it 'sets username and domain from final webfinger' do
        expect(account.username).to eq 'alice'
        expect(account.domain).to eq 'iscool.af'
      end

      include_examples 'sets profile data'
    end

    context 'when WebFinger returns a different URI' do
      let!(:webfinger) { { subject: 'acct:alice@example.com', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }

      before do
        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      end

      it 'fetches resource' do
        account
        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
      end

      it 'looks up webfinger' do
        account
        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
      end

      it 'does not create account' do
        expect(account).to be_nil
      end
    end

    context 'when WebFinger returns a different URI after a redirection' do
      let!(:webfinger) { { subject: 'acct:alice@iscool.af', links: [{ rel: 'self', href: 'https://example.com/bob' }] } }

      before do
        stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
        stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
        stub_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
      end

      it 'fetches resource' do
        account
        expect(a_request(:get, 'https://example.com/alice')).to have_been_made.once
      end

      it 'looks up webfinger' do
        account
        expect(a_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com')).to have_been_made.once
      end

      it 'looks up "redirected" webfinger' do
        account
        expect(a_request(:get, 'https://iscool.af/.well-known/webfinger?resource=acct:alice@iscool.af')).to have_been_made.once
      end

      it 'does not create account' do
        expect(account).to be_nil
      end
    end

    context 'with wrong id' do
      it 'does not create account' do
        expect(subject.call('https://fake.address/@foo', prefetched_body: Oj.dump(actor))).to be_nil
      end
    end
  end
end


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

class ActivityPub::FetchFeaturedCollectionService < BaseService
  include JsonLdHelper

  def call(account, **options)
    return if account.featured_collection_url.blank? || account.suspended? || account.local?

    @account = account
    @options = options
    @json    = fetch_resource(@account.featured_collection_url, true, local_follower)

    return unless supported_context?(@json)

    process_items(collection_items(@json))
  end

  private

  def collection_items(collection)
    collection = fetch_collection(collection['first']) if collection['first'].present?
    return unless collection.is_a?(Hash)

    case collection['type']
    when 'Collection', 'CollectionPage'
      collection['items']
    when 'OrderedCollection', 'OrderedCollectionPage'
      collection['orderedItems']
    end
  end

  def fetch_collection(collection_or_uri)
    return collection_or_uri if collection_or_uri.is_a?(Hash)
    return if non_matching_uri_hosts?(@account.uri, collection_or_uri)

    fetch_resource_without_id_validation(collection_or_uri, local_follower, true)
  end

  def process_items(items)
    return if items.nil?

    process_note_items(items) if @options[:note]
    process_hashtag_items(items) if @options[:hashtag]
  end

  def process_note_items(items)
    status_ids = items.filter_map do |item|
      next unless item.is_a?(String) || item['type'] == 'Note'

      uri = value_or_id(item)
      next if ActivityPub::TagManager.instance.local_uri?(uri) || non_matching_uri_hosts?(@account.uri, uri)

      status = ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: local_follower, expected_actor_uri: @account.uri, request_id: @options[:request_id])
      next unless status&.account_id == @account.id

      status.id
    rescue ActiveRecord::RecordInvalid => e
      Rails.logger.debug { "Invalid pinned status #{uri}: #{e.message}" }
      nil
    end

    to_remove = []
    to_add    = status_ids

    StatusPin.where(account: @account).pluck(:status_id).each do |status_id|
      if status_ids.include?(status_id)
        to_add.delete(status_id)
      else
        to_remove << status_id
      end
    end

    StatusPin.where(account: @account, status_id: to_remove).delete_all unless to_remove.empty?

    to_add.each do |status_id|
      StatusPin.create!(account: @account, status_id: status_id)
    end
  end

  def process_hashtag_items(items)
    names     = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.map { |name| HashtagNormalizer.new.normalize(name) }
    to_remove = []
    to_add    = names

    FeaturedTag.where(account: @account).map(&:name).each do |name|
      if names.include?(name)
        to_add.delete(name)
      else
        to_remove << name
      end
    end

    FeaturedTag.includes(:tag).where(account: @account, tags: { name: to_remove }).delete_all unless to_remove.empty?

    to_add.each do |name|
      FeaturedTag.create!(account: @account, name: name)
    end
  end

  def local_follower
    return @local_follower if defined?(@local_follower)

    @local_follower = @account.followers.local.without_suspended.first
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::FetchFeaturedCollectionService, type: :service do
  subject { described_class.new }

  let(:actor) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/account', featured_collection_url: 'https://example.com/account/pinned') }

  let!(:known_status) { Fabricate(:status, account: actor, uri: 'https://example.com/account/pinned/1') }

  let(:status_json_pinned_known) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      type: 'Note',
      id: 'https://example.com/account/pinned/known',
      content: 'foo',
      attributedTo: actor.uri,
      to: 'https://www.w3.org/ns/activitystreams#Public',
    }
  end

  let(:status_json_pinned_unknown_inlined) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      type: 'Note',
      id: 'https://example.com/account/pinned/unknown-inlined',
      content: 'foo',
      attributedTo: actor.uri,
      to: 'https://www.w3.org/ns/activitystreams#Public',
    }
  end

  let(:status_json_pinned_unknown_unreachable) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      type: 'Note',
      id: 'https://example.com/account/pinned/unknown-reachable',
      content: 'foo',
      attributedTo: actor.uri,
      to: 'https://www.w3.org/ns/activitystreams#Public',
    }
  end

  let(:featured_with_null) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: 'https://example.com/account/collections/featured',
      totalItems: 0,
      type: 'OrderedCollection',
    }
  end

  let(:items) do
    [
      'https://example.com/account/pinned/known', # known
      status_json_pinned_unknown_inlined, # unknown inlined
      'https://example.com/account/pinned/unknown-unreachable', # unknown unreachable
      'https://example.com/account/pinned/unknown-reachable', # unknown reachable
      'https://example.com/account/collections/featured', # featured with null
    ]
  end

  let(:payload) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      type: 'Collection',
      id: actor.featured_collection_url,
      items: items,
    }.with_indifferent_access
  end

  shared_examples 'sets pinned posts' do
    before do
      stub_request(:get, 'https://example.com/account/pinned/known').to_return(status: 200, body: Oj.dump(status_json_pinned_known))
      stub_request(:get, 'https://example.com/account/pinned/unknown-inlined').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_inlined))
      stub_request(:get, 'https://example.com/account/pinned/unknown-unreachable').to_return(status: 404)
      stub_request(:get, 'https://example.com/account/pinned/unknown-reachable').to_return(status: 200, body: Oj.dump(status_json_pinned_unknown_unreachable))
      stub_request(:get, 'https://example.com/account/collections/featured').to_return(status: 200, body: Oj.dump(featured_with_null))

      subject.call(actor, note: true, hashtag: false)
    end

    it 'sets expected posts as pinned posts' do
      expect(actor.pinned_statuses.pluck(:uri)).to contain_exactly(
        'https://example.com/account/pinned/known',
        'https://example.com/account/pinned/unknown-inlined',
        'https://example.com/account/pinned/unknown-reachable'
      )
    end
  end

  describe '#call' do
    context 'when the endpoint is a Collection' do
      before do
        stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
      end

      it_behaves_like 'sets pinned posts'
    end

    context 'when the endpoint is an OrderedCollection' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          type: 'OrderedCollection',
          id: actor.featured_collection_url,
          orderedItems: items,
        }.with_indifferent_access
      end

      before do
        stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
      end

      it_behaves_like 'sets pinned posts'
    end

    context 'when the endpoint is a paginated Collection' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          type: 'Collection',
          id: actor.featured_collection_url,
          first: {
            type: 'CollectionPage',
            partOf: actor.featured_collection_url,
            items: items,
          },
        }.with_indifferent_access
      end

      before do
        stub_request(:get, actor.featured_collection_url).to_return(status: 200, body: Oj.dump(payload))
      end

      it_behaves_like 'sets pinned posts'
    end
  end
end