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

class ActivityPub::FetchRemoteAccountService < ActivityPub::FetchRemoteActorService
  # 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)
    actor = super
    return actor if actor.nil? || actor.is_a?(Account)

    Rails.logger.debug { "Fetching account #{uri} failed: Expected Account, got #{actor.class.name}" }
    raise Error, "Expected Account, got #{actor.class.name}" unless suppress_errors
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::FetchRemoteAccountService, 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::FetchFeaturedTagsCollectionService < BaseService
  include JsonLdHelper

  def call(account, url)
    return if url.blank? || account.suspended? || account.local?

    @account = account
    @json    = fetch_resource(url, true, local_follower)

    return unless supported_context?(@json)

    process_items(collection_items(@json))
  end

  private

  def collection_items(collection)
    all_items = []

    collection = fetch_collection(collection['first']) if collection['first'].present?

    while collection.is_a?(Hash)
      items = case collection['type']
              when 'Collection', 'CollectionPage'
                collection['items']
              when 'OrderedCollection', 'OrderedCollectionPage'
                collection['orderedItems']
              end

      break if items.blank?

      all_items.concat(items)

      break if all_items.size >= FeaturedTag::LIMIT

      collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
    end

    all_items
  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)
    names            = items.filter_map { |item| item['type'] == 'Hashtag' && item['name']&.delete_prefix('#') }.take(FeaturedTag::LIMIT)
    tags             = names.index_by { |name| HashtagNormalizer.new.normalize(name) }
    normalized_names = tags.keys

    FeaturedTag.includes(:tag).references(:tag).where(account: @account).where.not(tag: { name: normalized_names }).delete_all

    FeaturedTag.includes(:tag).references(:tag).where(account: @account, tag: { name: normalized_names }).find_each do |featured_tag|
      featured_tag.update(name: tags.delete(featured_tag.tag.name))
    end

    tags.each_value 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::FetchFeaturedTagsCollectionService, type: :service do
  subject { described_class.new }

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

  let(:items) do
    [
      { type: 'Hashtag', href: 'https://example.com/account/tagged/foo', name: 'Foo' },
      { type: 'Hashtag', href: 'https://example.com/account/tagged/bar', name: 'bar' },
      { type: 'Hashtag', href: 'https://example.com/account/tagged/baz', name: 'baZ' },
    ]
  end

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

  shared_examples 'sets featured tags' do
    before do
      subject.call(actor, collection_url)
    end

    it 'sets expected tags as pinned tags' do
      expect(actor.featured_tags.map(&:display_name)).to match_array %w(Foo bar baZ)
    end
  end

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

      it_behaves_like 'sets featured tags'
    end

    context 'when the account already has featured tags' do
      before do
        stub_request(:get, collection_url).to_return(status: 200, body: Oj.dump(payload))

        actor.featured_tags.create!(name: 'FoO')
        actor.featured_tags.create!(name: 'baz')
        actor.featured_tags.create!(name: 'oh').update(name: nil)
      end

      it_behaves_like 'sets featured tags'
    end

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

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

      it_behaves_like 'sets featured tags'
    end

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

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

      it_behaves_like 'sets featured tags'
    end
  end
end


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

class ActivityPub::FetchRemoteStatusService < BaseService
  include JsonLdHelper
  include DomainControlHelper
  include Redisable

  DISCOVERIES_PER_REQUEST = 1000

  # Should be called when uri has already been checked for locality
  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
    return if domain_not_allowed?(uri)

    @request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
    @json = if prefetched_body.nil?
              fetch_resource(uri, id, on_behalf_of)
            else
              body_to_json(prefetched_body, compare_id: id ? uri : nil)
            end

    return unless supported_context?

    actor_uri     = nil
    activity_json = nil
    object_uri    = nil

    if expected_object_type?
      actor_uri     = value_or_id(first_of_value(@json['attributedTo']))
      activity_json = { 'type' => 'Create', 'actor' => actor_uri, 'object' => @json }
      object_uri    = uri_from_bearcap(@json['id'])
    elsif expected_activity_type?
      actor_uri     = value_or_id(first_of_value(@json['actor']))
      activity_json = @json
      object_uri    = uri_from_bearcap(value_or_id(@json['object']))
    end

    return if activity_json.nil? || object_uri.nil? || !trustworthy_attribution?(@json['id'], actor_uri)
    return if expected_actor_uri.present? && actor_uri != expected_actor_uri
    return ActivityPub::TagManager.instance.uri_to_resource(object_uri, Status) if ActivityPub::TagManager.instance.local_uri?(object_uri)

    actor = account_from_uri(actor_uri)

    return if actor.nil? || actor.suspended?

    # If we fetched a status that already exists, then we need to treat the
    # activity as an update rather than create
    activity_json['type'] = 'Update' if equals_or_includes_any?(activity_json['type'], %w(Create)) && Status.where(uri: object_uri, account_id: actor.id).exists?

    with_redis do |redis|
      discoveries = redis.incr("status_discovery_per_request:#{@request_id}")
      redis.expire("status_discovery_per_request:#{@request_id}", 5.minutes.seconds)
      return nil if discoveries > DISCOVERIES_PER_REQUEST
    end

    ActivityPub::Activity.factory(activity_json, actor, request_id: @request_id).perform
  end

  private

  def trustworthy_attribution?(uri, attributed_to)
    return false if uri.nil? || attributed_to.nil?

    Addressable::URI.parse(uri).normalized_host.casecmp(Addressable::URI.parse(attributed_to).normalized_host).zero?
  end

  def account_from_uri(uri)
    actor = ActivityPub::TagManager.instance.uri_to_resource(uri, Account)
    actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale?
    actor
  end

  def supported_context?
    super(@json)
  end

  def expected_activity_type?
    equals_or_includes_any?(@json['type'], %w(Create Announce))
  end

  def expected_object_type?
    equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do
  include ActionView::Helpers::TextHelper

  subject { described_class.new }

  let!(:sender) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar') }
  let!(:recipient) { Fabricate(:account) }

  let(:existing_status) { nil }

  let(:note) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: 'https://foo.bar/@foo/1234',
      type: 'Note',
      content: 'Lorem ipsum',
      attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
    }
  end

  before do
    stub_request(:get, 'https://foo.bar/watch?v=12345').to_return(status: 404, body: '')
    stub_request(:get, object[:id]).to_return(body: Oj.dump(object))
  end

  describe '#call' do
    before do
      existing_status
      subject.call(object[:id], prefetched_body: Oj.dump(object))
    end

    context 'with Note object' do
      let(:object) { note }

      it 'creates status' do
        status = sender.statuses.first

        expect(status).to_not be_nil
        expect(status.text).to eq 'Lorem ipsum'
      end
    end

    context 'with Video object' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1234',
          type: 'Video',
          name: 'Nyan Cat 10 hours remix',
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
          url: [
            {
              type: 'Link',
              mimeType: 'application/x-bittorrent',
              href: 'https://foo.bar/12345.torrent',
            },

            {
              type: 'Link',
              mimeType: 'text/html',
              href: 'https://foo.bar/watch?v=12345',
            },
          ],
        }
      end

      it 'creates status' do
        status = sender.statuses.first

        expect(status).to_not be_nil
        expect(status.url).to eq 'https://foo.bar/watch?v=12345'
        expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remixhttps://foo.bar/watch?v=12345'
      end
    end

    context 'with Audio object' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1234',
          type: 'Audio',
          name: 'Nyan Cat 10 hours remix',
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
          url: [
            {
              type: 'Link',
              mimeType: 'application/x-bittorrent',
              href: 'https://foo.bar/12345.torrent',
            },

            {
              type: 'Link',
              mimeType: 'text/html',
              href: 'https://foo.bar/watch?v=12345',
            },
          ],
        }
      end

      it 'creates status' do
        status = sender.statuses.first

        expect(status).to_not be_nil
        expect(status.url).to eq 'https://foo.bar/watch?v=12345'
        expect(strip_tags(status.text)).to eq 'Nyan Cat 10 hours remixhttps://foo.bar/watch?v=12345'
      end
    end

    context 'with Event object' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1234',
          type: 'Event',
          name: "Let's change the world",
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
        }
      end

      it 'creates status' do
        status = sender.statuses.first

        expect(status).to_not be_nil
        expect(status.url).to eq 'https://foo.bar/@foo/1234'
        expect(strip_tags(status.text)).to eq "Let's change the worldhttps://foo.bar/@foo/1234"
      end
    end

    context 'with wrong id' do
      let(:note) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://real.address/@foo/1234',
          type: 'Note',
          content: 'Lorem ipsum',
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
        }
      end

      let(:object) do
        temp = note.dup
        temp[:id] = 'https://fake.address/@foo/5678'
        temp
      end

      it 'does not create status' do
        expect(sender.statuses.first).to be_nil
      end
    end

    context 'with a valid Create activity' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1234/create',
          type: 'Create',
          actor: ActivityPub::TagManager.instance.uri_for(sender),
          object: note,
        }
      end

      it 'creates status' do
        status = sender.statuses.first

        expect(status).to_not be_nil
        expect(status.uri).to eq note[:id]
        expect(status.text).to eq note[:content]
      end
    end

    context 'with a Create activity with a mismatching id' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1234/create',
          type: 'Create',
          actor: ActivityPub::TagManager.instance.uri_for(sender),
          object: {
            id: 'https://real.address/@foo/1234',
            type: 'Note',
            content: 'Lorem ipsum',
            attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
          },
        }
      end

      it 'does not create status' do
        expect(sender.statuses.first).to be_nil
      end
    end

    context 'when status already exists' do
      let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id]) }

      context 'with a Note object' do
        let(:object) { note.merge(updated: '2021-09-08T22:39:25Z') }

        it 'updates status' do
          existing_status.reload
          expect(existing_status.text).to eq 'Lorem ipsum'
          expect(existing_status.edits).to_not be_empty
        end
      end

      context 'with a Create activity' do
        let(:object) do
          {
            '@context': 'https://www.w3.org/ns/activitystreams',
            id: 'https://foo.bar/@foo/1234/create',
            type: 'Create',
            actor: ActivityPub::TagManager.instance.uri_for(sender),
            object: note.merge(updated: '2021-09-08T22:39:25Z'),
          }
        end

        it 'updates status' do
          existing_status.reload
          expect(existing_status.text).to eq 'Lorem ipsum'
          expect(existing_status.edits).to_not be_empty
        end
      end
    end
  end

  context 'with statuses referencing other statuses' do
    before do
      stub_const 'ActivityPub::FetchRemoteStatusService::DISCOVERIES_PER_REQUEST', 5
    end

    context 'when using inReplyTo' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1',
          type: 'Note',
          content: 'Lorem ipsum',
          inReplyTo: 'https://foo.bar/@foo/2',
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
        }
      end

      before do
        8.times do |i|
          status_json = {
            '@context': 'https://www.w3.org/ns/activitystreams',
            id: "https://foo.bar/@foo/#{i}",
            type: 'Note',
            content: 'Lorem ipsum',
            inReplyTo: "https://foo.bar/@foo/#{i + 1}",
            attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
            to: 'as:Public',
          }.with_indifferent_access
          stub_request(:get, "https://foo.bar/@foo/#{i}").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' })
        end
      end

      it 'creates at least some statuses' do
        expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) }.to change { sender.statuses.count }.by_at_least(2)
      end

      it 'creates no more account than the limit allows' do
        expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) }.to change { sender.statuses.count }.by_at_most(5)
      end
    end

    context 'when using replies' do
      let(:object) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://foo.bar/@foo/1',
          type: 'Note',
          content: 'Lorem ipsum',
          replies: {
            type: 'Collection',
            id: 'https://foo.bar/@foo/1/replies',
            first: {
              type: 'CollectionPage',
              partOf: 'https://foo.bar/@foo/1/replies',
              items: ['https://foo.bar/@foo/2'],
            },
          },
          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
        }
      end

      before do
        8.times do |i|
          status_json = {
            '@context': 'https://www.w3.org/ns/activitystreams',
            id: "https://foo.bar/@foo/#{i}",
            type: 'Note',
            content: 'Lorem ipsum',
            replies: {
              type: 'Collection',
              id: "https://foo.bar/@foo/#{i}/replies",
              first: {
                type: 'CollectionPage',
                partOf: "https://foo.bar/@foo/#{i}/replies",
                items: ["https://foo.bar/@foo/#{i + 1}"],
              },
            },
            attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
            to: 'as:Public',
          }.with_indifferent_access
          stub_request(:get, "https://foo.bar/@foo/#{i}").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' })
        end
      end

      it 'creates at least some statuses' do
        expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) }.to change { sender.statuses.count }.by_at_least(2)
      end

      it 'creates no more account than the limit allows' do
        expect { subject.call(object[:id], prefetched_body: Oj.dump(object)) }.to change { sender.statuses.count }.by_at_most(5)
      end
    end
  end
end


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

class ActivityPub::ProcessAccountService < BaseService
  include JsonLdHelper
  include DomainControlHelper
  include Redisable
  include Lockable

  SUBDOMAINS_RATELIMIT = 10
  DISCOVERIES_PER_REQUEST = 400

  # Should be called with confirmed valid JSON
  # and WebFinger-resolved username and domain
  def call(username, domain, json, options = {})
    return if json['inbox'].blank? || unsupported_uri_scheme?(json['id']) || domain_not_allowed?(domain)

    @options     = options
    @json        = json
    @uri         = @json['id']
    @username    = username
    @domain      = TagManager.instance.normalize_domain(domain)
    @collections = {}

    # The key does not need to be unguessable, it just needs to be somewhat unique
    @options[:request_id] ||= "#{Time.now.utc.to_i}-#{username}@#{domain}"

    with_redis_lock("process_account:#{@uri}") do
      @account            = Account.remote.find_by(uri: @uri) if @options[:only_key]
      @account          ||= Account.find_remote(@username, @domain)
      @old_public_key     = @account&.public_key
      @old_protocol       = @account&.protocol
      @suspension_changed = false

      if @account.nil?
        with_redis do |redis|
          return nil if redis.pfcount("unique_subdomains_for:#{PublicSuffix.domain(@domain, ignore_private: true)}") >= SUBDOMAINS_RATELIMIT

          discoveries = redis.incr("discovery_per_request:#{@options[:request_id]}")
          redis.expire("discovery_per_request:#{@options[:request_id]}", 5.minutes.seconds)
          return nil if discoveries > DISCOVERIES_PER_REQUEST
        end

        create_account
      end

      update_account
      process_tags

      process_duplicate_accounts! if @options[:verified_webfinger]
    end

    after_protocol_change! if protocol_changed?
    after_key_change! if key_changed? && !@options[:signed_with_known_key]
    clear_tombstones! if key_changed?
    after_suspension_change! if suspension_changed?

    unless @options[:only_key] || @account.suspended?
      check_featured_collection! if @account.featured_collection_url.present?
      check_featured_tags_collection! if @json['featuredTags'].present?
      check_links! if @account.fields.any?(&:requires_verification?)
    end

    @account
  rescue Oj::ParseError
    nil
  end

  private

  def create_account
    @account = Account.new
    @account.protocol          = :activitypub
    @account.username          = @username
    @account.domain            = @domain
    @account.private_key       = nil
    @account.suspended_at      = domain_block.created_at if auto_suspend?
    @account.suspension_origin = :local if auto_suspend?
    @account.silenced_at       = domain_block.created_at if auto_silence?

    set_immediate_protocol_attributes!

    @account.save!
  end

  def update_account
    @account.last_webfingered_at = Time.now.utc unless @options[:only_key]
    @account.protocol            = :activitypub

    set_suspension!
    set_immediate_protocol_attributes!
    set_fetchable_key! unless @account.suspended? && @account.suspension_origin_local?
    set_immediate_attributes! unless @account.suspended?
    set_fetchable_attributes! unless @options[:only_key] || @account.suspended?

    @account.save_with_optional_media!
  end

  def set_immediate_protocol_attributes!
    @account.inbox_url               = @json['inbox'] || ''
    @account.outbox_url              = @json['outbox'] || ''
    @account.shared_inbox_url        = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
    @account.followers_url           = @json['followers'] || ''
    @account.url                     = url || @uri
    @account.uri                     = @uri
    @account.actor_type              = actor_type
    @account.created_at              = @json['published'] if @json['published'].present?
  end

  def set_immediate_attributes!
    @account.featured_collection_url = @json['featured'] || ''
    @account.devices_url             = @json['devices'] || ''
    @account.display_name            = @json['name'] || ''
    @account.note                    = @json['summary'] || ''
    @account.locked                  = @json['manuallyApprovesFollowers'] || false
    @account.fields                  = property_values || {}
    @account.also_known_as           = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
    @account.discoverable            = @json['discoverable'] || false
    @account.indexable               = @json['indexable'] || false
    @account.memorial                = @json['memorial'] || false
  end

  def set_fetchable_key!
    @account.public_key = public_key || ''
  end

  def set_fetchable_attributes!
    begin
      @account.avatar_remote_url = image_url('icon') || '' unless skip_download?
      @account.avatar = nil if @account.avatar_remote_url.blank?
    rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
      RedownloadAvatarWorker.perform_in(rand(30..600).seconds, @account.id)
    end
    begin
      @account.header_remote_url = image_url('image') || '' unless skip_download?
      @account.header = nil if @account.header_remote_url.blank?
    rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
      RedownloadHeaderWorker.perform_in(rand(30..600).seconds, @account.id)
    end
    @account.statuses_count    = outbox_total_items    if outbox_total_items.present?
    @account.following_count   = following_total_items if following_total_items.present?
    @account.followers_count   = followers_total_items if followers_total_items.present?
    @account.hide_collections  = following_private? || followers_private?
    @account.moved_to_account  = @json['movedTo'].present? ? moved_account : nil
  end

  def set_suspension!
    return if @account.suspended? && @account.suspension_origin_local?

    if @account.suspended? && !@json['suspended']
      @account.unsuspend!
      @suspension_changed = true
    elsif !@account.suspended? && @json['suspended']
      @account.suspend!(origin: :remote)
      @suspension_changed = true
    end
  end

  def after_protocol_change!
    ActivityPub::PostUpgradeWorker.perform_async(@account.domain)
  end

  def after_key_change!
    RefollowWorker.perform_async(@account.id)
  end

  def after_suspension_change!
    if @account.suspended?
      Admin::SuspensionWorker.perform_async(@account.id)
    else
      Admin::UnsuspensionWorker.perform_async(@account.id)
    end
  end

  def check_featured_collection!
    ActivityPub::SynchronizeFeaturedCollectionWorker.perform_async(@account.id, { 'hashtag' => @json['featuredTags'].blank?, 'request_id' => @options[:request_id] })
  end

  def check_featured_tags_collection!
    ActivityPub::SynchronizeFeaturedTagsCollectionWorker.perform_async(@account.id, @json['featuredTags'])
  end

  def check_links!
    VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), @account.id)
  end

  def process_duplicate_accounts!
    return unless Account.where(uri: @account.uri).where.not(id: @account.id).exists?

    AccountMergingWorker.perform_async(@account.id)
  end

  def actor_type
    if @json['type'].is_a?(Array)
      @json['type'].find { |type| ActivityPub::FetchRemoteAccountService::SUPPORTED_TYPES.include?(type) }
    else
      @json['type']
    end
  end

  def image_url(key)
    value = first_of_value(@json[key])

    return if value.nil?
    return value['url'] if value.is_a?(Hash)

    image = fetch_resource_without_id_validation(value)
    image['url'] if image
  end

  def public_key
    value = first_of_value(@json['publicKey'])

    return if value.nil?
    return value['publicKeyPem'] if value.is_a?(Hash)

    key = fetch_resource_without_id_validation(value)
    key['publicKeyPem'] if key
  end

  def url
    return if @json['url'].blank?

    url_candidate = url_to_href(@json['url'], 'text/html')

    if unsupported_uri_scheme?(url_candidate) || mismatching_origin?(url_candidate)
      nil
    else
      url_candidate
    end
  end

  def property_values
    return unless @json['attachment'].is_a?(Array)

    as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
  end

  def mismatching_origin?(url)
    needle   = Addressable::URI.parse(url).host
    haystack = Addressable::URI.parse(@uri).host

    !haystack.casecmp(needle).zero?
  end

  def outbox_total_items
    collection_info('outbox').first
  end

  def following_total_items
    collection_info('following').first
  end

  def followers_total_items
    collection_info('followers').first
  end

  def following_private?
    !collection_info('following').last
  end

  def followers_private?
    !collection_info('followers').last
  end

  def collection_info(type)
    return [nil, nil] if @json[type].blank?
    return @collections[type] if @collections.key?(type)

    collection = fetch_resource_without_id_validation(@json[type])

    total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
    has_first_page = collection.is_a?(Hash) && collection['first'].present?
    @collections[type] = [total_items, has_first_page]
  rescue HTTP::Error, OpenSSL::SSL::SSLError, Mastodon::LengthValidationError
    @collections[type] = [nil, nil]
  end

  def moved_account
    account   = ActivityPub::TagManager.instance.uri_to_resource(@json['movedTo'], Account)
    account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id])
    account
  end

  def skip_download?
    @account.suspended? || domain_block&.reject_media?
  end

  def auto_suspend?
    domain_block&.suspend?
  end

  def auto_silence?
    domain_block&.silence?
  end

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

    @domain_block = DomainBlock.rule_for(@domain)
  end

  def key_changed?
    !@old_public_key.nil? && @old_public_key != @account.public_key
  end

  def suspension_changed?
    @suspension_changed
  end

  def clear_tombstones!
    Tombstone.where(account_id: @account.id).delete_all
  end

  def protocol_changed?
    !@old_protocol.nil? && @old_protocol != @account.protocol
  end

  def process_tags
    return if @json['tag'].blank?

    as_array(@json['tag']).each do |tag|
      process_emoji tag if equals_or_includes?(tag['type'], 'Emoji')
    end
  end

  def process_emoji(tag)
    return if skip_download?
    return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank?

    shortcode = tag['name'].delete(':')
    image_url = tag['icon']['url']
    uri       = tag['id']
    updated   = tag['updated']
    emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)

    return unless emoji.nil? || image_url != emoji.image_remote_url || (updated && updated >= emoji.updated_at)

    emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri)
    emoji.image_remote_url = image_url
    emoji.save
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  context 'with property values' do
    let(:payload) do
      {
        id: 'https://foo.test',
        type: 'Actor',
        inbox: 'https://foo.test/inbox',
        attachment: [
          { type: 'PropertyValue', name: 'Pronouns', value: 'They/them' },
          { type: 'PropertyValue', name: 'Occupation', value: 'Unit test' },
          { type: 'PropertyValue', name: 'non-string', value: %w(foo bar) },
        ],
      }.with_indifferent_access
    end

    it 'parses out of attachment' do
      account = subject.call('alice', 'example.com', payload)
      expect(account.fields).to be_a Array
      expect(account.fields.size).to eq 2
      expect(account.fields[0]).to be_a Account::Field
      expect(account.fields[0].name).to eq 'Pronouns'
      expect(account.fields[0].value).to eq 'They/them'
      expect(account.fields[1]).to be_a Account::Field
      expect(account.fields[1].name).to eq 'Occupation'
      expect(account.fields[1].value).to eq 'Unit test'
    end
  end

  context 'when account is not suspended' do
    subject { described_class.new.call('alice', 'example.com', payload) }

    let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com') }

    let(:payload) do
      {
        id: 'https://foo.test',
        type: 'Actor',
        inbox: 'https://foo.test/inbox',
        suspended: true,
      }.with_indifferent_access
    end

    before do
      allow(Admin::SuspensionWorker).to receive(:perform_async)
    end

    it 'suspends account remotely' do
      expect(subject.suspended?).to be true
      expect(subject.suspension_origin_remote?).to be true
    end

    it 'queues suspension worker' do
      subject
      expect(Admin::SuspensionWorker).to have_received(:perform_async)
    end
  end

  context 'when account is suspended' do
    subject { described_class.new.call('alice', 'example.com', payload) }

    let!(:account) { Fabricate(:account, username: 'alice', domain: 'example.com', display_name: '') }

    let(:payload) do
      {
        id: 'https://foo.test',
        type: 'Actor',
        inbox: 'https://foo.test/inbox',
        suspended: false,
        name: 'Hoge',
      }.with_indifferent_access
    end

    before do
      allow(Admin::UnsuspensionWorker).to receive(:perform_async)

      account.suspend!(origin: suspension_origin)
    end

    context 'when locally' do
      let(:suspension_origin) { :local }

      it 'does not unsuspend it' do
        expect(subject.suspended?).to be true
      end

      it 'does not update any attributes' do
        expect(subject.display_name).to_not eq 'Hoge'
      end
    end

    context 'when remotely' do
      let(:suspension_origin) { :remote }

      it 'unsuspends it' do
        expect(subject.suspended?).to be false
      end

      it 'queues unsuspension worker' do
        subject
        expect(Admin::UnsuspensionWorker).to have_received(:perform_async)
      end

      it 'updates attributes' do
        expect(subject.display_name).to eq 'Hoge'
      end
    end
  end

  context 'when discovering many subdomains in a short timeframe' do
    subject do
      8.times do |i|
        domain = "test#{i}.testdomain.com"
        json = {
          id: "https://#{domain}/users/1",
          type: 'Actor',
          inbox: "https://#{domain}/inbox",
        }.with_indifferent_access
        described_class.new.call('alice', domain, json)
      end
    end

    before do
      stub_const 'ActivityPub::ProcessAccountService::SUBDOMAINS_RATELIMIT', 5
    end

    it 'creates at least some accounts' do
      expect { subject }.to change { Account.remote.count }.by_at_least(2)
    end

    it 'creates no more account than the limit allows' do
      expect { subject }.to change { Account.remote.count }.by_at_most(5)
    end
  end

  context 'when Accounts referencing other accounts' do
    let(:payload) do
      {
        '@context': ['https://www.w3.org/ns/activitystreams'],
        id: 'https://foo.test/users/1',
        type: 'Person',
        inbox: 'https://foo.test/inbox',
        featured: 'https://foo.test/users/1/featured',
        preferredUsername: 'user1',
      }.with_indifferent_access
    end

    before do
      stub_const 'ActivityPub::ProcessAccountService::DISCOVERIES_PER_REQUEST', 5

      8.times do |i|
        actor_json = {
          '@context': ['https://www.w3.org/ns/activitystreams'],
          id: "https://foo.test/users/#{i}",
          type: 'Person',
          inbox: 'https://foo.test/inbox',
          featured: "https://foo.test/users/#{i}/featured",
          preferredUsername: "user#{i}",
        }.with_indifferent_access
        status_json = {
          '@context': ['https://www.w3.org/ns/activitystreams'],
          id: "https://foo.test/users/#{i}/status",
          attributedTo: "https://foo.test/users/#{i}",
          type: 'Note',
          content: "@user#{i + 1} test",
          tag: [
            {
              type: 'Mention',
              href: "https://foo.test/users/#{i + 1}",
              name: "@user#{i + 1}",
            },
          ],
          to: ['as:Public', "https://foo.test/users/#{i + 1}"],
        }.with_indifferent_access
        featured_json = {
          '@context': ['https://www.w3.org/ns/activitystreams'],
          id: "https://foo.test/users/#{i}/featured",
          type: 'OrderedCollection',
          totalItems: 1,
          orderedItems: [status_json],
        }.with_indifferent_access
        webfinger = {
          subject: "acct:user#{i}@foo.test",
          links: [{ rel: 'self', href: "https://foo.test/users/#{i}" }],
        }.with_indifferent_access
        stub_request(:get, "https://foo.test/users/#{i}").to_return(status: 200, body: actor_json.to_json, headers: { 'Content-Type': 'application/activity+json' })
        stub_request(:get, "https://foo.test/users/#{i}/featured").to_return(status: 200, body: featured_json.to_json, headers: { 'Content-Type': 'application/activity+json' })
        stub_request(:get, "https://foo.test/users/#{i}/status").to_return(status: 200, body: status_json.to_json, headers: { 'Content-Type': 'application/activity+json' })
        stub_request(:get, "https://foo.test/.well-known/webfinger?resource=acct:user#{i}@foo.test").to_return(body: webfinger.to_json, headers: { 'Content-Type': 'application/jrd+json' })
      end
    end

    it 'creates at least some accounts' do
      expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_least(2)
    end

    it 'creates no more account than the limit allows' do
      expect { subject.call('user1', 'foo.test', payload) }.to change { Account.remote.count }.by_at_most(5)
    end
  end
end


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

class ActivityPub::FetchRepliesService < BaseService
  include JsonLdHelper

  def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil)
    @account = parent_status.account
    @allow_synchronous_requests = allow_synchronous_requests

    @items = collection_items(collection_or_uri)
    return if @items.nil?

    FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id }] }

    @items
  end

  private

  def collection_items(collection_or_uri)
    collection = fetch_collection(collection_or_uri)
    return unless collection.is_a?(Hash)

    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 unless @allow_synchronous_requests
    return if non_matching_uri_hosts?(@account.uri, collection_or_uri)

    fetch_resource_without_id_validation(collection_or_uri, nil, true)
  end

  def filtered_replies
    # Only fetch replies to the same server as the original status to avoid
    # amplification attacks.

    # Also limit to 5 fetched replies to limit potential for DoS.
    @items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(5)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let(:actor)          { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account') }
  let(:status)         { Fabricate(:status, account: actor) }
  let(:collection_uri) { 'http://example.com/replies/1' }

  let(:items) do
    [
      'http://example.com/self-reply-1',
      'http://example.com/self-reply-2',
      'http://example.com/self-reply-3',
      'http://other.com/other-reply-1',
      'http://other.com/other-reply-2',
      'http://other.com/other-reply-3',
      'http://example.com/self-reply-4',
      'http://example.com/self-reply-5',
      'http://example.com/self-reply-6',
    ]
  end

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

  describe '#call' do
    context 'when the payload is a Collection with inlined replies' do
      context 'when passing the collection itself' do
        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, payload)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end

      context 'when passing the URL to the collection' do
        before do
          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
        end

        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, collection_uri)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end
    end

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

      context 'when passing the collection itself' do
        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, payload)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end

      context 'when passing the URL to the collection' do
        before do
          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
        end

        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, collection_uri)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end
    end

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

      context 'when passing the collection itself' do
        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, payload)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end

      context 'when passing the URL to the collection' do
        before do
          stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload))
        end

        it 'spawns workers for up to 5 replies on the same server' do
          allow(FetchReplyWorker).to receive(:push_bulk)

          subject.call(status, collection_uri)

          expect(FetchReplyWorker).to have_received(:push_bulk).with(['http://example.com/self-reply-1', 'http://example.com/self-reply-2', 'http://example.com/self-reply-3', 'http://example.com/self-reply-4', 'http://example.com/self-reply-5'])
        end
      end
    end
  end
end


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

class ActivityPub::SynchronizeFollowersService < BaseService
  include JsonLdHelper
  include Payloadable

  def call(account, partial_collection_url)
    @account = account

    items = collection_items(partial_collection_url)
    return if items.nil?

    # There could be unresolved accounts (hence the call to .compact) but this
    # should never happen in practice, since in almost all cases we keep an
    # Account record, and should we not do that, we should have sent a Delete.
    # In any case there is not much we can do if that occurs.
    @expected_followers = items.filter_map { |uri| ActivityPub::TagManager.instance.uri_to_resource(uri, Account) }

    remove_unexpected_local_followers!
    handle_unexpected_outgoing_follows!
  end

  private

  def remove_unexpected_local_followers!
    @account.followers.local.where.not(id: @expected_followers.map(&:id)).reorder(nil).find_each do |unexpected_follower|
      UnfollowService.new.call(unexpected_follower, @account)
    end
  end

  def handle_unexpected_outgoing_follows!
    @expected_followers.each do |expected_follower|
      next if expected_follower.following?(@account)

      if expected_follower.requested?(@account)
        # For some reason the follow request went through but we missed it
        expected_follower.follow_requests.find_by(target_account: @account)&.authorize!
      else
        # Since we were not aware of the follow from our side, we do not have an
        # ID for it that we can include in the Undo activity. For this reason,
        # the Undo may not work with software that relies exclusively on
        # matching activity IDs and not the actor and target
        follow = Follow.new(account: expected_follower, target_account: @account)
        ActivityPub::DeliveryWorker.perform_async(build_undo_follow_json(follow), follow.account_id, follow.target_account.inbox_url)
      end
    end
  end

  def build_undo_follow_json(follow)
    Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer))
  end

  def collection_items(collection_or_uri)
    collection = fetch_collection(collection_or_uri)
    return unless collection.is_a?(Hash)

    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, nil, true)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let(:actor)          { Fabricate(:account, domain: 'example.com', uri: 'http://example.com/account', inbox_url: 'http://example.com/inbox') }
  let(:alice)          { Fabricate(:account, username: 'alice') }
  let(:bob)            { Fabricate(:account, username: 'bob') }
  let(:eve)            { Fabricate(:account, username: 'eve') }
  let(:mallory)        { Fabricate(:account, username: 'mallory') }
  let(:collection_uri) { 'http://example.com/partial-followers' }

  let(:items) do
    [
      ActivityPub::TagManager.instance.uri_for(alice),
      ActivityPub::TagManager.instance.uri_for(eve),
      ActivityPub::TagManager.instance.uri_for(mallory),
    ]
  end

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

  shared_examples 'synchronizes followers' do
    before do
      alice.follow!(actor)
      bob.follow!(actor)
      mallory.request_follow!(actor)

      allow(ActivityPub::DeliveryWorker).to receive(:perform_async)

      subject.call(actor, collection_uri)
    end

    it 'keeps expected followers' do
      expect(alice.following?(actor)).to be true
    end

    it 'removes local followers not in the remote list' do
      expect(bob.following?(actor)).to be false
    end

    it 'converts follow requests to follow relationships when they have been accepted' do
      expect(mallory.following?(actor)).to be true
    end

    it 'sends an Undo Follow to the actor' do
      expect(ActivityPub::DeliveryWorker).to have_received(:perform_async).with(anything, eve.id, actor.inbox_url)
    end
  end

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

      it_behaves_like 'synchronizes followers'
    end

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

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

      it_behaves_like 'synchronizes followers'
    end

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

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

      it_behaves_like 'synchronizes followers'
    end
  end
end


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

class ActivityPub::ProcessStatusUpdateService < BaseService
  include JsonLdHelper
  include Redisable
  include Lockable

  def call(status, activity_json, object_json, request_id: nil)
    raise ArgumentError, 'Status has unsaved changes' if status.changed?

    @activity_json             = activity_json
    @json                      = object_json
    @status_parser             = ActivityPub::Parser::StatusParser.new(@json)
    @uri                       = @status_parser.uri
    @status                    = status
    @account                   = status.account
    @media_attachments_changed = false
    @poll_changed              = false
    @request_id                = request_id

    # Only native types can be updated at the moment
    return @status if !expected_type? || already_updated_more_recently?

    if @status_parser.edited_at.present? && (@status.edited_at.nil? || @status_parser.edited_at > @status.edited_at)
      handle_explicit_update!
    else
      handle_implicit_update!
    end

    @status
  end

  private

  def handle_explicit_update!
    last_edit_date = @status.edited_at.presence || @status.created_at

    # Only allow processing one create/update per status at a time
    with_redis_lock("create:#{@uri}") do
      Status.transaction do
        record_previous_edit!
        update_media_attachments!
        update_poll!
        update_immediate_attributes!
        update_metadata!
        create_edits!
      end

      download_media_files!
      queue_poll_notifications!

      next unless significant_changes?

      reset_preview_card!
      broadcast_updates!
    end

    forward_activity! if significant_changes? && @status_parser.edited_at > last_edit_date
  end

  def handle_implicit_update!
    with_redis_lock("create:#{@uri}") do
      update_poll!(allow_significant_changes: false)
      queue_poll_notifications!
    end
  end

  def update_media_attachments!
    previous_media_attachments     = @status.media_attachments.to_a
    previous_media_attachments_ids = @status.ordered_media_attachment_ids || previous_media_attachments.map(&:id)
    @next_media_attachments        = []

    as_array(@json['attachment']).each do |attachment|
      media_attachment_parser = ActivityPub::Parser::MediaAttachmentParser.new(attachment)

      next if media_attachment_parser.remote_url.blank? || @next_media_attachments.size > 4

      begin
        media_attachment   = previous_media_attachments.find { |previous_media_attachment| previous_media_attachment.remote_url == media_attachment_parser.remote_url }
        media_attachment ||= MediaAttachment.new(account: @account, remote_url: media_attachment_parser.remote_url)

        # If a previously existing media attachment was significantly updated, mark
        # media attachments as changed even if none were added or removed
        @media_attachments_changed = true if media_attachment_parser.significantly_changes?(media_attachment)

        media_attachment.description          = media_attachment_parser.description
        media_attachment.focus                = media_attachment_parser.focus
        media_attachment.thumbnail_remote_url = media_attachment_parser.thumbnail_remote_url
        media_attachment.blurhash             = media_attachment_parser.blurhash
        media_attachment.status_id            = @status.id
        media_attachment.skip_download        = unsupported_media_type?(media_attachment_parser.file_content_type) || skip_download?
        media_attachment.save!

        @next_media_attachments << media_attachment
      rescue Addressable::URI::InvalidURIError => e
        Rails.logger.debug { "Invalid URL in attachment: #{e}" }
      end
    end

    @status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)

    @media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
  end

  def download_media_files!
    @next_media_attachments.each do |media_attachment|
      next if media_attachment.skip_download

      media_attachment.download_file! if media_attachment.remote_url_previously_changed?
      media_attachment.download_thumbnail! if media_attachment.thumbnail_remote_url_previously_changed?
      media_attachment.save
    rescue Mastodon::UnexpectedResponseError, HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError
      RedownloadMediaWorker.perform_in(rand(30..600).seconds, media_attachment.id)
    rescue Seahorse::Client::NetworkingError => e
      Rails.logger.warn "Error storing media attachment: #{e}"
    end

    @status.media_attachments.reload
  end

  def update_poll!(allow_significant_changes: true)
    previous_poll        = @status.preloadable_poll
    @previous_expires_at = previous_poll&.expires_at
    poll_parser          = ActivityPub::Parser::PollParser.new(@json)

    if poll_parser.valid?
      poll = previous_poll || @account.polls.new(status: @status)

      # If for some reasons the options were changed, it invalidates all previous
      # votes, so we need to remove them
      @poll_changed = true if poll_parser.significantly_changes?(poll)
      return if @poll_changed && !allow_significant_changes

      poll.last_fetched_at = Time.now.utc
      poll.options         = poll_parser.options
      poll.multiple        = poll_parser.multiple
      poll.expires_at      = poll_parser.expires_at
      poll.voters_count    = poll_parser.voters_count
      poll.cached_tallies  = poll_parser.cached_tallies
      poll.reset_votes! if @poll_changed
      poll.save!

      @status.poll_id = poll.id
    elsif previous_poll.present?
      return unless allow_significant_changes

      previous_poll.destroy!
      @poll_changed = true
      @status.poll_id = nil
    end
  end

  def update_immediate_attributes!
    @status.text         = @status_parser.text || ''
    @status.spoiler_text = @status_parser.spoiler_text || ''
    @status.sensitive    = @account.sensitized? || @status_parser.sensitive || false
    @status.language     = @status_parser.language

    @significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed

    @status.edited_at = @status_parser.edited_at if significant_changes?

    @status.save!
  end

  def update_metadata!
    @raw_tags     = []
    @raw_mentions = []
    @raw_emojis   = []

    as_array(@json['tag']).each do |tag|
      if equals_or_includes?(tag['type'], 'Hashtag')
        @raw_tags << tag['name'] if tag['name'].present?
      elsif equals_or_includes?(tag['type'], 'Mention')
        @raw_mentions << tag['href'] if tag['href'].present?
      elsif equals_or_includes?(tag['type'], 'Emoji')
        @raw_emojis << tag
      end
    end

    update_tags!
    update_mentions!
    update_emojis!
  end

  def update_tags!
    @status.tags = Tag.find_or_create_by_names(@raw_tags)
  end

  def update_mentions!
    previous_mentions = @status.active_mentions.includes(:account).to_a
    current_mentions  = []

    @raw_mentions.each do |href|
      next if href.blank?

      account   = ActivityPub::TagManager.instance.uri_to_resource(href, Account)
      account ||= ActivityPub::FetchRemoteAccountService.new.call(href, request_id: @request_id)

      next if account.nil?

      mention   = previous_mentions.find { |x| x.account_id == account.id }
      mention ||= account.mentions.new(status: @status)

      current_mentions << mention
    end

    current_mentions.each do |mention|
      mention.save if mention.new_record?
    end

    # If previous mentions are no longer contained in the text, convert them
    # to silent mentions, since withdrawing access from someone who already
    # received a notification might be more confusing
    removed_mentions = previous_mentions - current_mentions

    Mention.where(id: removed_mentions.map(&:id)).update_all(silent: true) unless removed_mentions.empty?
  end

  def update_emojis!
    return if skip_download?

    @raw_emojis.each do |raw_emoji|
      custom_emoji_parser = ActivityPub::Parser::CustomEmojiParser.new(raw_emoji)

      next if custom_emoji_parser.shortcode.blank? || custom_emoji_parser.image_remote_url.blank?

      emoji = CustomEmoji.find_by(shortcode: custom_emoji_parser.shortcode, domain: @account.domain)

      next unless emoji.nil? || custom_emoji_parser.image_remote_url != emoji.image_remote_url || (custom_emoji_parser.updated_at && custom_emoji_parser.updated_at >= emoji.updated_at)

      begin
        emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: custom_emoji_parser.shortcode, uri: custom_emoji_parser.uri)
        emoji.image_remote_url = custom_emoji_parser.image_remote_url
        emoji.save
      rescue Seahorse::Client::NetworkingError => e
        Rails.logger.warn "Error storing emoji: #{e}"
      end
    end
  end

  def expected_type?
    equals_or_includes_any?(@json['type'], %w(Note Question))
  end

  def record_previous_edit!
    @previous_edit = @status.build_snapshot(at_time: @status.created_at, rate_limit: false) if @status.edits.empty?
  end

  def create_edits!
    return unless significant_changes?

    @previous_edit&.save!
    @status.snapshot!(account_id: @account.id, rate_limit: false)
  end

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

    @skip_download ||= DomainBlock.reject_media?(@account.domain)
  end

  def unsupported_media_type?(mime_type)
    mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type)
  end

  def significant_changes?
    @significant_changes
  end

  def text_significantly_changed?
    return false unless @status.text_changed?

    old, new = @status.text_change
    HtmlAwareFormatter.new(old, false).to_s != HtmlAwareFormatter.new(new, false).to_s
  end

  def already_updated_more_recently?
    @status.edited_at.present? && @status_parser.edited_at.present? && @status.edited_at > @status_parser.edited_at
  end

  def reset_preview_card!
    @status.reset_preview_card!
    LinkCrawlWorker.perform_in(rand(1..59).seconds, @status.id)
  end

  def broadcast_updates!
    ::DistributionWorker.perform_async(@status.id, { 'update' => true })
  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, and people have voted, schedule a notification

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

    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 forward_activity!
    forwarder.forward! if forwarder.forwardable?
  end

  def forwarder
    @forwarder ||= ActivityPub::Forwarder.new(@account, @activity_json, @status)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

def poll_option_json(name, votes)
  { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } }
end

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

  let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) }
  let(:payload) do
    {
      '@context': 'https://www.w3.org/ns/activitystreams',
      id: 'foo',
      type: 'Note',
      summary: 'Show more',
      content: 'Hello universe',
      updated: '2021-09-08T22:39:25Z',
      tag: [
        { type: 'Hashtag', name: 'hoge' },
        { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) },
      ],
    }
  end
  let(:json) { Oj.load(Oj.dump(payload)) }

  let(:alice) { Fabricate(:account) }
  let(:bob) { Fabricate(:account) }

  let(:mentions) { [] }
  let(:tags) { [] }
  let(:media_attachments) { [] }

  before do
    mentions.each { |a| Fabricate(:mention, status: status, account: a) }
    tags.each { |t| status.tags << t }
    media_attachments.each { |m| status.media_attachments << m }
  end

  describe '#call' do
    it 'updates text' do
      subject.call(status, json, json)
      expect(status.reload.text).to eq 'Hello universe'
    end

    it 'updates content warning' do
      subject.call(status, json, json)
      expect(status.reload.spoiler_text).to eq 'Show more'
    end

    context 'when the changes are only in sanitized-out HTML' do
      let!(:status) { Fabricate(:status, text: '
Hello world joinmastodon.org
', account: Fabricate(:account, domain: 'example.com')) }

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          updated: '2021-09-08T22:39:25Z',
          content: '
Hello world joinmastodon.org
',
        }
      end

      before do
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.edited?).to be false
      end
    end

    context 'when the status has not been explicitly edited' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          content: 'Updated text',
        }
      end

      before do
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.reload.edited?).to be false
      end

      it 'does not update the text' do
        expect(status.reload.text).to eq 'Hello world'
      end
    end

    context 'when the status has not been explicitly edited and features a poll' do
      let(:account) { Fabricate(:account, domain: 'example.com') }
      let!(:expiration) { 10.days.from_now.utc }
      let!(:status) do
        Fabricate(:status,
                  text: 'Hello world',
                  account: account,
                  poll_attributes: {
                    options: %w(Foo Bar),
                    account: account,
                    multiple: false,
                    hide_totals: false,
                    expires_at: expiration,
                  })
      end

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://example.com/foo',
          type: 'Question',
          content: 'Hello world',
          endTime: expiration.iso8601,
          oneOf: [
            poll_option_json('Foo', 4),
            poll_option_json('Bar', 3),
          ],
        }
      end

      before do
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.reload.edited?).to be false
      end

      it 'does not update the text' do
        expect(status.reload.text).to eq 'Hello world'
      end

      it 'updates tallies' do
        expect(status.poll.reload.cached_tallies).to eq [4, 3]
      end
    end

    context 'when the status changes a poll despite being not explicitly marked as updated' do
      let(:account) { Fabricate(:account, domain: 'example.com') }
      let!(:expiration) { 10.days.from_now.utc }
      let!(:status) do
        Fabricate(:status,
                  text: 'Hello world',
                  account: account,
                  poll_attributes: {
                    options: %w(Foo Bar),
                    account: account,
                    multiple: false,
                    hide_totals: false,
                    expires_at: expiration,
                  })
      end

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'https://example.com/foo',
          type: 'Question',
          content: 'Hello world',
          endTime: expiration.iso8601,
          oneOf: [
            poll_option_json('Foo', 4),
            poll_option_json('Bar', 3),
            poll_option_json('Baz', 3),
          ],
        }
      end

      before do
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.reload.edited?).to be false
      end

      it 'does not update the text' do
        expect(status.reload.text).to eq 'Hello world'
      end

      it 'does not update tallies' do
        expect(status.poll.reload.cached_tallies).to eq [0, 0]
      end
    end

    context 'when receiving an edit older than the latest processed' do
      before do
        status.snapshot!(at_time: status.created_at, rate_limit: false)
        status.update!(text: 'Hello newer world', edited_at: Time.now.utc)
        status.snapshot!(rate_limit: false)
      end

      it 'does not create any edits' do
        expect { subject.call(status, json, json) }.to_not(change { status.reload.edits.pluck(&:id) })
      end

      it 'does not update the text, spoiler_text or edited_at' do
        expect { subject.call(status, json, json) }.to_not(change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] })
      end
    end

    context 'with no changes at all' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          content: 'Hello world',
        }
      end

      before do
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.edited?).to be false
      end
    end

    context 'with no changes and originally with no ordered_media_attachment_ids' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          content: 'Hello world',
        }
      end

      before do
        status.update(ordered_media_attachment_ids: nil)
        subject.call(status, json, json)
      end

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

      it 'does not mark status as edited' do
        expect(status.edited?).to be false
      end
    end

    context 'when originally without tags' do
      before do
        subject.call(status, json, json)
      end

      it 'updates tags' do
        expect(status.tags.reload.map(&:name)).to eq %w(hoge)
      end
    end

    context 'when originally with tags' do
      let(:tags) { [Fabricate(:tag, name: 'test'), Fabricate(:tag, name: 'foo')] }

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          summary: 'Show more',
          content: 'Hello universe',
          updated: '2021-09-08T22:39:25Z',
          tag: [
            { type: 'Hashtag', name: 'foo' },
          ],
        }
      end

      before do
        subject.call(status, json, json)
      end

      it 'updates tags' do
        expect(status.tags.reload.map(&:name)).to eq %w(foo)
      end
    end

    context 'when originally without mentions' do
      before do
        subject.call(status, json, json)
      end

      it 'updates mentions' do
        expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
      end
    end

    context 'when originally with mentions' do
      let(:mentions) { [alice, bob] }

      before do
        subject.call(status, json, json)
      end

      it 'updates mentions' do
        expect(status.active_mentions.reload.map(&:account_id)).to eq [alice.id]
      end
    end

    context 'when originally without media attachments' do
      before do
        stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png'))
        subject.call(status, json, json)
      end

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          content: 'Hello universe',
          updated: '2021-09-08T22:39:25Z',
          attachment: [
            { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
          ],
        }
      end

      it 'updates media attachments' do
        media_attachment = status.reload.ordered_media_attachments.first

        expect(media_attachment).to_not be_nil
        expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
      end

      it 'fetches the attachment' do
        expect(a_request(:get, 'https://example.com/foo.png')).to have_been_made
      end

      it 'records media change in edit' do
        expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
      end
    end

    context 'when originally with media attachments' do
      let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }

      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Note',
          content: 'Hello universe',
          updated: '2021-09-08T22:39:25Z',
          attachment: [
            { type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png', name: 'A picture' },
          ],
        }
      end

      before do
        allow(RedownloadMediaWorker).to receive(:perform_async)
        subject.call(status, json, json)
      end

      it 'updates the existing media attachment in-place' do
        media_attachment = status.media_attachments.ordered.reload.first

        expect(media_attachment).to_not be_nil
        expect(media_attachment.remote_url).to eq 'https://example.com/foo.png'
        expect(media_attachment.description).to eq 'A picture'
      end

      it 'does not queue redownload for the existing media attachment' do
        expect(RedownloadMediaWorker).to_not have_received(:perform_async)
      end

      it 'updates media attachments' do
        expect(status.ordered_media_attachments.map(&:remote_url)).to eq %w(https://example.com/foo.png)
      end

      it 'records media change in edit' do
        expect(status.edits.reload.last.ordered_media_attachment_ids).to_not be_empty
      end
    end

    context 'when originally with a poll' do
      before do
        poll = Fabricate(:poll, status: status)
        status.update(preloadable_poll: poll)
        subject.call(status, json, json)
      end

      it 'removes poll' do
        expect(status.reload.poll).to be_nil
      end

      it 'records media change in edit' do
        expect(status.edits.reload.last.poll_options).to be_nil
      end
    end

    context 'when originally without a poll' do
      let(:payload) do
        {
          '@context': 'https://www.w3.org/ns/activitystreams',
          id: 'foo',
          type: 'Question',
          content: 'Hello universe',
          updated: '2021-09-08T22:39:25Z',
          closed: true,
          oneOf: [
            { type: 'Note', name: 'Foo' },
            { type: 'Note', name: 'Bar' },
            { type: 'Note', name: 'Baz' },
          ],
        }
      end

      before do
        subject.call(status, json, json)
      end

      it 'creates a poll' do
        poll = status.reload.poll

        expect(poll).to_not be_nil
        expect(poll.options).to eq %w(Foo Bar Baz)
      end

      it 'records media change in edit' do
        expect(status.edits.reload.last.poll_options).to eq %w(Foo Bar Baz)
      end
    end

    it 'creates edit history' do
      subject.call(status, json, json)
      expect(status.edits.reload.map(&:text)).to eq ['Hello world', 'Hello universe']
    end

    it 'sets edited timestamp' do
      subject.call(status, json, json)
      expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC'
    end
  end
end


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

class UploadService
  # Temporarily introduced for upload API: https://gitlab.com/gitlab-org/gitlab/-/issues/325788
  attr_accessor :override_max_attachment_size

  def initialize(model, file, uploader_class = FileUploader, **uploader_context)
    @model = model
    @file = file
    @uploader_class = uploader_class
    @uploader_context = uploader_context
  end

  def execute
    return unless file && file.size <= max_attachment_size

    uploader = uploader_class.new(model, nil, **uploader_context)
    uploader.store!(file)

    uploader
  end

  private

  attr_reader :model, :file, :uploader_class, :uploader_context

  def max_attachment_size
    override_max_attachment_size || Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe UploadService, feature_category: :shared do
  describe 'File service' do
    before do
      @user = create(:user)
      @project = create(:project, creator_id: @user.id, namespace: @user.namespace)
    end

    context 'for valid gif file' do
      before do
        gif = fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')
        @link_to_file = upload_file(@project, gif)
      end

      it { expect(@link_to_file).to have_key(:alt) }
      it { expect(@link_to_file).to have_key(:url) }
      it { expect(@link_to_file).to have_value('banana_sample') }
      it { expect(@link_to_file[:url]).to match('banana_sample.gif') }
    end

    context 'for valid png file' do
      before do
        png = fixture_file_upload('spec/fixtures/dk.png',
          'image/png')
        @link_to_file = upload_file(@project, png)
      end

      it { expect(@link_to_file).to have_key(:alt) }
      it { expect(@link_to_file).to have_key(:url) }
      it { expect(@link_to_file).to have_value('dk') }
      it { expect(@link_to_file[:url]).to match('dk.png') }
    end

    context 'for valid jpg file' do
      before do
        jpg = fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg')
        @link_to_file = upload_file(@project, jpg)
      end

      it { expect(@link_to_file).to have_key(:alt) }
      it { expect(@link_to_file).to have_key(:url) }
      it { expect(@link_to_file).to have_value('rails_sample') }
      it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
    end

    context 'for txt file' do
      before do
        txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain')
        @link_to_file = upload_file(@project, txt)
      end

      it { expect(@link_to_file).to have_key(:alt) }
      it { expect(@link_to_file).to have_key(:url) }
      it { expect(@link_to_file).to have_value('doc_sample.txt') }
      it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
    end

    context 'for too large a file' do
      before do
        txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain')
        allow(txt).to receive(:size) { 1000.megabytes.to_i }
        @link_to_file = upload_file(@project, txt)
      end

      it { expect(@link_to_file).to eq({}) }
    end

    describe '#override_max_attachment_size' do
      let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') }
      let(:service) { described_class.new(@project, txt, FileUploader) }

      subject { service.execute.to_h }

      before do
        allow(txt).to receive(:size) { 100.megabytes.to_i }
      end

      it 'allows the upload' do
        service.override_max_attachment_size = 101.megabytes

        expect(subject.keys).to eq(%i[alt url markdown])
      end

      it 'disallows the upload' do
        service.override_max_attachment_size = 99.megabytes

        expect(subject).to eq({})
      end
    end
  end

  def upload_file(project, file)
    described_class.new(project, file, FileUploader).execute.to_h
  end
end


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

class NoteSummary
  attr_reader :note
  attr_reader :metadata

  def initialize(noteable, project, author, body, action: nil, commit_count: nil, created_at: nil)
    @note = { noteable: noteable,
              created_at: created_at || noteable.system_note_timestamp,
              project: project, author: author, note: body }
    @metadata = { action: action, commit_count: commit_count }.compact

    if action == 'description' && noteable.saved_description_version
      @metadata[:description_version] = noteable.saved_description_version
    end

    set_commit_params if note[:noteable].is_a?(Commit)
  end

  def metadata?
    metadata.present?
  end

  def set_commit_params
    note.merge!(noteable_type: 'Commit', commit_id: note[:noteable].id)
    note[:noteable] = nil
  end
end

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe NoteSummary, feature_category: :code_review_workflow do
  let(:project)  { build(:project) }
  let(:noteable) { build(:issue) }
  let(:user)     { build(:user) }

  def create_note_summary
    described_class.new(noteable, project, user, 'note', action: 'icon', commit_count: 5)
  end

  describe '#metadata?' do
    it 'returns true when metadata present' do
      expect(create_note_summary.metadata?).to be_truthy
    end

    it 'returns false when metadata not present' do
      expect(described_class.new(noteable, project, user, 'note').metadata?).to be_falsey
    end
  end

  describe '#note' do
    it 'returns note hash' do
      freeze_time do
        expect(create_note_summary.note).to eq(
          noteable: noteable,
          project: project,
          author: user,
          note: 'note',
          created_at: Time.current
        )
      end
    end

    context 'when noteable is a commit' do
      let(:noteable) { build(:commit, system_note_timestamp: Time.zone.at(43)) }

      it 'returns note hash specific to commit' do
        expect(create_note_summary.note).to eq(
          noteable: nil, project: project, author: user, note: 'note',
          noteable_type: 'Commit', commit_id: noteable.id,
          created_at: Time.zone.at(43)
        )
      end
    end
  end

  describe '#metadata' do
    it 'returns metadata hash' do
      expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5)
    end

    context 'description action and noteable has saved_description_version' do
      before do
        noteable.saved_description_version = 1
      end

      subject { described_class.new(noteable, project, user, 'note', action: 'description') }

      it 'sets the description_version metadata' do
        expect(subject.metadata).to include(description_version: 1)
      end
    end
  end
end


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

class AutoMergeService < BaseService
  include Gitlab::Utils::StrongMemoize

  STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds'
  STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze

  class << self
    def all_strategies_ordered_by_preference
      STRATEGIES
    end

    def get_service_class(strategy)
      return unless all_strategies_ordered_by_preference.include?(strategy)

      "::AutoMerge::#{strategy.camelize}Service".constantize
    end
  end

  def execute(merge_request, strategy = nil)
    strategy ||= preferred_strategy(merge_request)
    service = get_service_instance(merge_request, strategy)

    return :failed unless service&.available_for?(merge_request)

    service.execute(merge_request)
  end

  def update(merge_request)
    return :failed unless merge_request.auto_merge_enabled?

    strategy = merge_request.auto_merge_strategy
    get_service_instance(merge_request, strategy).update(merge_request)
  end

  def process(merge_request)
    return unless merge_request.auto_merge_enabled?

    strategy = merge_request.auto_merge_strategy
    get_service_instance(merge_request, strategy).process(merge_request)
  end

  def cancel(merge_request)
    return error("Can't cancel the automatic merge", 406) unless merge_request.auto_merge_enabled?

    strategy = merge_request.auto_merge_strategy
    get_service_instance(merge_request, strategy).cancel(merge_request)
  end

  def abort(merge_request, reason)
    return error("Can't abort the automatic merge", 406) unless merge_request.auto_merge_enabled?

    strategy = merge_request.auto_merge_strategy
    get_service_instance(merge_request, strategy).abort(merge_request, reason)
  end

  def available_strategies(merge_request)
    self.class.all_strategies_ordered_by_preference.select do |strategy|
      get_service_instance(merge_request, strategy).available_for?(merge_request)
    end
  end

  def preferred_strategy(merge_request)
    available_strategies(merge_request).first
  end

  private

  def get_service_instance(merge_request, strategy)
    strong_memoize("service_instance_#{merge_request.id}_#{strategy}") do
      self.class.get_service_class(strategy)&.new(project, current_user, params)
    end
  end
end

AutoMergeService.prepend_mod_with('AutoMergeService')

```

Response:
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe AutoMergeService, feature_category: :code_review_workflow do
  let_it_be(:project) { create(:project, :repository) }
  let_it_be(:user) { create(:user) }

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

  before_all do
    project.add_maintainer(user)
  end

  describe '.all_strategies_ordered_by_preference' do
    subject { described_class.all_strategies_ordered_by_preference }

    it 'returns all strategies in preference order' do
      if Gitlab.ee?
        is_expected.to contain_exactly(
          AutoMergeService::STRATEGY_MERGE_TRAIN,
          AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS,
          AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS,
          AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
        )
      else
        is_expected.to eq([AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS])
      end
    end
  end

  describe '#available_strategies' do
    subject { service.available_strategies(merge_request) }

    let(:merge_request) do
      create(:merge_request, source_project: project)
    end

    let(:pipeline_status) { :running }

    before do
      create(
        :ci_pipeline,
        pipeline_status,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha,
        project: merge_request.source_project
      )

      merge_request.update_head_pipeline
    end

    it 'returns available strategies' do
      is_expected.to include('merge_when_pipeline_succeeds')
    end

    context 'when the head piipeline succeeded' do
      let(:pipeline_status) { :success }

      it 'returns available strategies' do
        is_expected.to be_empty
      end
    end
  end

  describe '#preferred_strategy' do
    subject { service.preferred_strategy(merge_request) }

    let(:merge_request) do
      create(:merge_request, source_project: project)
    end

    let(:pipeline_status) { :running }

    before do
      create(
        :ci_pipeline,
        pipeline_status,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha,
        project: merge_request.source_project
      )

      merge_request.update_head_pipeline

      stub_licensed_features(merge_request_approvers: true) if Gitlab.ee?
    end

    it 'returns preferred strategy', if: Gitlab.ee? do
      is_expected.to eq('merge_when_checks_pass')
    end

    it 'returns preferred strategy', unless: Gitlab.ee? do
      is_expected.to eq('merge_when_pipeline_succeeds')
    end

    context 'when the head pipeline succeeded' do
      let(:pipeline_status) { :success }

      it 'returns available strategies' do
        is_expected.to be_nil
      end
    end
  end

  describe '.get_service_class' do
    subject { described_class.get_service_class(strategy) }

    let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }

    it 'returns service instance' do
      is_expected.to eq(AutoMerge::MergeWhenPipelineSucceedsService)
    end

    context 'when strategy is not present' do
      let(:strategy) {}

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

  describe '#execute' do
    subject { service.execute(merge_request, strategy) }

    let(:merge_request) do
      create(:merge_request, source_project: project)
    end

    let(:pipeline_status) { :running }
    let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }

    before do
      create(
        :ci_pipeline,
        pipeline_status,
        ref: merge_request.source_branch,
        sha: merge_request.diff_head_sha,
        project: merge_request.source_project
      )

      merge_request.update_head_pipeline
    end

    it 'delegates to a relevant service instance' do
      expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
        expect(service).to receive(:execute).with(merge_request)
      end

      subject
    end

    context 'when the head pipeline succeeded' do
      let(:pipeline_status) { :success }

      it 'returns failed' do
        is_expected.to eq(:failed)
      end
    end

    context 'when strategy is not specified' do
      let(:strategy) {}

      before do
        stub_licensed_features(merge_request_approvers: true) if Gitlab.ee?
      end

      it 'chooses the most preferred strategy', if: Gitlab.ee? do
        is_expected.to eq(:merge_when_checks_pass)
      end

      it 'chooses the most preferred strategy', unless: Gitlab.ee? do
        is_expected.to eq(:merge_when_pipeline_succeeds)
      end
    end
  end

  describe '#update' do
    subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang

    context 'when auto merge is enabled' do
      let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

      it 'delegates to a relevant service instance' do
        expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
          expect(service).to receive(:update).with(merge_request)
        end

        subject
      end
    end

    context 'when auto merge is not enabled' do
      let(:merge_request) { create(:merge_request) }

      it 'returns failed' do
        is_expected.to eq(:failed)
      end
    end
  end

  describe '#process' do
    subject { service.process(merge_request) }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

    it 'delegates to a relevant service instance' do
      expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
        expect(service).to receive(:process).with(merge_request)
      end

      subject
    end

    context 'when auto merge is not enabled' do
      let(:merge_request) { create(:merge_request) }

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

  describe '#cancel' do
    subject { service.cancel(merge_request) }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

    it 'delegates to a relevant service instance' do
      expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
        expect(service).to receive(:cancel).with(merge_request)
      end

      subject
    end

    context 'when auto merge is not enabled' do
      let(:merge_request) { create(:merge_request) }

      it 'returns error' do
        expect(subject[:message]).to eq("Can't cancel the automatic merge")
        expect(subject[:status]).to eq(:error)
        expect(subject[:http_status]).to eq(406)
      end
    end
  end

  describe '#abort' do
    subject { service.abort(merge_request, error) }

    let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
    let(:error) { 'an error' }

    it 'delegates to a relevant service instance' do
      expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
        expect(service).to receive(:abort).with(merge_request, error)
      end

      subject
    end

    context 'when auto merge is not enabled' do
      let(:merge_request) { create(:merge_request) }

      it 'returns error' do
        expect(subject[:message]).to eq("Can't abort the automatic merge")
        expect(subject[:status]).to eq(:error)
        expect(subject[:http_status]).to eq(406)
      end
    end
  end
end