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

class UnallowDomainService < BaseService
  include DomainControlHelper

  def call(domain_allow)
    suspend_accounts!(domain_allow.domain) if limited_federation_mode?

    domain_allow.destroy
  end

  private

  def suspend_accounts!(domain)
    Account.where(domain: domain).in_batches.update_all(suspended_at: Time.now.utc)
    AfterUnallowDomainWorker.perform_async(domain)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
  let!(:bad_status_harassment) { Fabricate(:status, account: bad_account, text: 'You suck') }
  let!(:bad_status_mean) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
  let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_mean, file: attachment_fixture('attachment.jpg')) }
  let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) }
  let!(:domain_allow) { Fabricate(:domain_allow, domain: 'evil.org') }

  context 'with limited federation mode' do
    before do
      allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true)
    end

    describe '#call' do
      before do
        subject.call(domain_allow)
      end

      it 'removes the allowed domain' do
        expect(DomainAllow.allowed?('evil.org')).to be false
      end

      it 'removes remote accounts from that domain' do
        expect(Account.where(domain: 'evil.org').exists?).to be false
      end

      it 'removes the remote accounts\'s statuses and media attachments' do
        expect { bad_status_harassment.reload }.to raise_exception ActiveRecord::RecordNotFound
        expect { bad_status_mean.reload }.to raise_exception ActiveRecord::RecordNotFound
        expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound
      end
    end
  end

  context 'without limited federation mode' do
    before do
      allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(false)
    end

    describe '#call' do
      before do
        subject.call(domain_allow)
      end

      it 'removes the allowed domain' do
        expect(DomainAllow.allowed?('evil.org')).to be false
      end

      it 'does not remove accounts from that domain' do
        expect(Account.where(domain: 'evil.org').exists?).to be true
      end

      it 'removes the remote accounts\'s statuses and media attachments' do
        expect { bad_status_harassment.reload }.to_not raise_error
        expect { bad_status_mean.reload }.to_not raise_error
        expect { bad_attachment.reload }.to_not raise_error
      end
    end
  end
end


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

class ReportService < BaseService
  include Payloadable

  def call(source_account, target_account, options = {})
    @source_account = source_account
    @target_account = target_account
    @status_ids     = options.delete(:status_ids).presence || []
    @comment        = options.delete(:comment).presence || ''
    @category       = options[:rule_ids].present? ? 'violation' : (options.delete(:category).presence || 'other')
    @rule_ids       = options.delete(:rule_ids).presence
    @options        = options

    raise ActiveRecord::RecordNotFound if @target_account.unavailable?

    create_report!
    notify_staff!

    if forward?
      forward_to_origin!
      forward_to_replied_to!
    end

    @report
  end

  private

  def create_report!
    @report = @source_account.reports.create!(
      target_account: @target_account,
      status_ids: reported_status_ids,
      comment: @comment,
      uri: @options[:uri],
      forwarded: forward_to_origin?,
      category: @category,
      rule_ids: @rule_ids
    )
  end

  def notify_staff!
    return if @report.unresolved_siblings?

    User.those_who_can(:manage_reports).includes(:account).find_each do |u|
      LocalNotificationWorker.perform_async(u.account_id, @report.id, 'Report', 'admin.report')
      AdminMailer.with(recipient: u.account).new_report(@report).deliver_later if u.allows_report_emails?
    end
  end

  def forward_to_origin!
    return unless forward_to_origin?

    # Send report to the server where the account originates from
    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, @target_account.inbox_url)
  end

  def forward_to_replied_to!
    # Send report to servers to which the account was replying to, so they also have a chance to act
    inbox_urls = Account.remote.where(domain: forward_to_domains).where(id: Status.where(id: reported_status_ids).where.not(in_reply_to_account_id: nil).select(:in_reply_to_account_id)).inboxes - [@target_account.inbox_url, @target_account.shared_inbox_url]

    inbox_urls.each do |inbox_url|
      ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
    end
  end

  def forward?
    !@target_account.local? && ActiveModel::Type::Boolean.new.cast(@options[:forward])
  end

  def forward_to_origin?
    forward? && forward_to_domains.include?(@target_account.domain)
  end

  def forward_to_domains
    @forward_to_domains ||= (@options[:forward_to_domains] || [@target_account.domain]).filter_map { |domain| TagManager.instance.normalize_domain(domain&.strip) }.uniq
  end

  def reported_status_ids
    return AccountStatusesFilter.new(@target_account, @source_account).results.with_discarded.find(Array(@status_ids)).pluck(:id) if @source_account.local?

    # If the account making reports is remote, it is likely anonymized so we have to relax the requirements for attaching statuses.
    domain = @source_account.domain.to_s.downcase
    has_followers = @target_account.followers.where(Account.arel_table[:domain].lower.eq(domain)).exists?
    visibility = has_followers ? %i(public unlisted private) : %i(public unlisted)
    scope = @target_account.statuses.with_discarded
    scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain)))
    # Allow missing posts to not drop reports that include e.g. a deleted post
    scope.where(id: Array(@status_ids)).pluck(:id)
  end

  def payload
    Oj.dump(serialize_payload(@report, ActivityPub::FlagSerializer, account: some_local_account))
  end

  def some_local_account
    @some_local_account ||= Account.representative
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let(:source_account) { Fabricate(:account) }
  let(:target_account) { Fabricate(:account) }

  context 'with a local account' do
    it 'has a uri' do
      report = subject.call(source_account, target_account)
      expect(report.uri).to_not be_nil
    end
  end

  context 'with a remote account' do
    let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
    let(:forward) { false }

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

    context 'when forward is true' do
      let(:forward) { true }

      it 'sends ActivityPub payload when forward is true' do
        subject.call(source_account, remote_account, forward: forward)
        expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
      end

      it 'has an uri' do
        report = subject.call(source_account, remote_account, forward: forward)
        expect(report.uri).to_not be_nil
      end

      context 'when reporting a reply on a different remote server' do
        let(:remote_thread_account) { Fabricate(:account, domain: 'foo.com', protocol: :activitypub, inbox_url: 'http://foo.com/inbox') }
        let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) }

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

        context 'when forward_to_domains includes both the replied-to domain and the origin domain' do
          it 'sends ActivityPub payload to both the author of the replied-to post and the reported user' do
            subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_account.domain, remote_thread_account.domain])
            expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made
            expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
          end
        end

        context 'when forward_to_domains includes only the replied-to domain' do
          it 'sends ActivityPub payload only to the author of the replied-to post' do
            subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_thread_account.domain])
            expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made
            expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
          end
        end

        context 'when forward_to_domains does not include the replied-to domain' do
          it 'does not send ActivityPub payload to the author of the replied-to post' do
            subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward)
            expect(a_request(:post, 'http://foo.com/inbox')).to_not have_been_made
          end
        end
      end

      context 'when reporting a reply on the same remote server as the person being replied-to' do
        let(:remote_thread_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
        let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) }

        context 'when forward_to_domains includes both the replied-to domain and the origin domain' do
          it 'sends ActivityPub payload only once' do
            subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward, forward_to_domains: [remote_account.domain])
            expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
          end
        end

        context 'when forward_to_domains does not include the replied-to domain' do
          it 'sends ActivityPub payload only once' do
            subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward)
            expect(a_request(:post, 'http://example.com/inbox')).to have_been_made.once
          end
        end
      end
    end

    context 'when forward is false' do
      it 'does not send anything' do
        subject.call(source_account, remote_account, forward: forward)
        expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
      end
    end
  end

  context 'when the reported status is a DM' do
    subject do
      -> { described_class.new.call(source_account, target_account, status_ids: [status.id]) }
    end

    let(:status) { Fabricate(:status, account: target_account, visibility: :direct) }

    context 'when it is addressed to the reporter' do
      before do
        status.mentions.create(account: source_account)
      end

      it 'creates a report' do
        expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1)
      end

      it 'attaches the DM to the report' do
        subject.call
        expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]]
      end
    end

    context 'when it is not addressed to the reporter' do
      it 'errors out' do
        expect { subject.call }.to raise_error(ActiveRecord::RecordNotFound)
      end
    end

    context 'when the reporter is remote' do
      let(:source_account) { Fabricate(:account, domain: 'example.com', uri: 'https://example.com/users/1') }

      context 'when it is addressed to the reporter' do
        before do
          status.mentions.create(account: source_account)
        end

        it 'creates a report' do
          expect { subject.call }.to change { target_account.targeted_reports.count }.from(0).to(1)
        end

        it 'attaches the DM to the report' do
          subject.call
          expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[status.id]]
        end
      end

      context 'when it is not addressed to the reporter' do
        it 'does not add the DM to the report' do
          subject.call
          expect(target_account.targeted_reports.pluck(:status_ids)).to eq [[]]
        end
      end
    end
  end

  context 'when other reports already exist for the same target' do
    subject do
      -> {  described_class.new.call(source_account, target_account) }
    end

    let!(:other_report) { Fabricate(:report, target_account: target_account) }

    before do
      ActionMailer::Base.deliveries.clear
      source_account.user.settings['notification_emails.report'] = true
      source_account.user.save
    end

    it 'does not send an e-mail' do
      expect { subject.call }.to_not change(ActionMailer::Base.deliveries, :count).from(0)
    end
  end
end


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

class FavouriteService < BaseService
  include Authorization
  include Payloadable

  # Favourite a status and notify remote user
  # @param [Account] account
  # @param [Status] status
  # @return [Favourite]
  def call(account, status)
    authorize_with account, status, :favourite?

    favourite = Favourite.find_by(account: account, status: status)

    return favourite unless favourite.nil?

    favourite = Favourite.create!(account: account, status: status)

    Trends.statuses.register(status)

    create_notification(favourite)
    bump_potential_friendship(account, status)

    favourite
  end

  private

  def create_notification(favourite)
    status = favourite.status

    if status.account.local?
      LocalNotificationWorker.perform_async(status.account_id, favourite.id, 'Favourite', 'favourite')
    elsif status.account.activitypub?
      ActivityPub::DeliveryWorker.perform_async(build_json(favourite), favourite.account_id, status.account.inbox_url)
    end
  end

  def bump_potential_friendship(account, status)
    ActivityTracker.increment('activity:interactions')
    return if account.following?(status.account_id)

    PotentialFriendshipTracker.record(account.id, status.account_id, :favourite)
  end

  def build_json(favourite)
    Oj.dump(serialize_payload(favourite, ActivityPub::LikeSerializer))
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

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

  describe 'local' do
    let(:bob)    { Fabricate(:account) }
    let(:status) { Fabricate(:status, account: bob) }

    before do
      subject.call(sender, status)
    end

    it 'creates a favourite' do
      expect(status.favourites.first).to_not be_nil
    end
  end

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

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

    it 'creates a favourite' do
      expect(status.favourites.first).to_not be_nil
    end

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


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

class ClearDomainMediaService < BaseService
  attr_reader :domain_block

  def call(domain_block)
    @domain_block = domain_block
    clear_media! if domain_block.reject_media?
  end

  private

  def clear_media!
    clear_account_images!
    clear_account_attachments!
    clear_emojos!
  end

  def clear_account_images!
    blocked_domain_accounts.reorder(nil).find_in_batches do |accounts|
      AttachmentBatch.new(Account, accounts).clear
    end
  end

  def clear_account_attachments!
    media_from_blocked_domain.reorder(nil).find_in_batches do |attachments|
      AttachmentBatch.new(MediaAttachment, attachments).clear
    end
  end

  def clear_emojos!
    emojis_from_blocked_domains.find_in_batches do |custom_emojis|
      AttachmentBatch.new(CustomEmoji, custom_emojis).delete
    end
  end

  def blocked_domain
    domain_block.domain
  end

  def blocked_domain_accounts
    Account.by_domain_and_subdomains(blocked_domain)
  end

  def media_from_blocked_domain
    MediaAttachment.joins(:account).merge(blocked_domain_accounts)
  end

  def emojis_from_blocked_domains
    CustomEmoji.by_domain_and_subdomains(blocked_domain)
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
  let!(:bad_status_plain) { Fabricate(:status, account: bad_account, text: 'You suck') }
  let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
  let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_with_attachment, file: attachment_fixture('attachment.jpg')) }

  describe 'for a silence with reject media' do
    before do
      subject.call(DomainBlock.create!(domain: 'evil.org', severity: :silence, reject_media: true))
    end

    it 'leaves the domains status and attachments, but clears media' do
      expect { bad_status_plain.reload }.to_not raise_error
      expect { bad_status_with_attachment.reload }.to_not raise_error
      expect { bad_attachment.reload }.to_not raise_error
      expect(bad_attachment.file.exists?).to be false
    end
  end
end


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

class UnblockService < BaseService
  include Payloadable

  def call(account, target_account)
    return unless account.blocking?(target_account)

    unblock = account.unblock!(target_account)
    create_notification(unblock) if !target_account.local? && target_account.activitypub?
    unblock
  end

  private

  def create_notification(unblock)
    ActivityPub::DeliveryWorker.perform_async(build_json(unblock), unblock.account_id, unblock.target_account.inbox_url)
  end

  def build_json(unblock)
    Oj.dump(serialize_payload(unblock, ActivityPub::UndoBlockSerializer))
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

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

  describe 'local' do
    let(:bob) { Fabricate(:account) }

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

    it 'destroys the blocking relation' do
      expect(sender.blocking?(bob)).to be false
    end
  end

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

    before do
      sender.block!(bob)
      stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
      subject.call(sender, bob)
    end

    it 'destroys the blocking relation' do
      expect(sender.blocking?(bob)).to be false
    end

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


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

class NotifyService < BaseService
  include Redisable

  NON_EMAIL_TYPES = %i(
    admin.report
    admin.sign_up
    update
    poll
    status
  ).freeze

  def call(recipient, type, activity)
    @recipient    = recipient
    @activity     = activity
    @notification = Notification.new(account: @recipient, type: type, activity: @activity)

    return if recipient.user.nil? || blocked?

    @notification.save!

    # It's possible the underlying activity has been deleted
    # between the save call and now
    return if @notification.activity.nil?

    push_notification!
    push_to_conversation! if direct_message?
    send_email! if email_needed?
  rescue ActiveRecord::RecordInvalid
    nil
  end

  private

  def blocked_mention?
    FeedManager.instance.filter?(:mentions, @notification.mention.status, @recipient)
  end

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

    @following_sender = @recipient.following?(@notification.from_account) || @recipient.requested?(@notification.from_account)
  end

  def optional_non_follower?
    @recipient.user.settings['interactions.must_be_follower']  && !@notification.from_account.following?(@recipient)
  end

  def optional_non_following?
    @recipient.user.settings['interactions.must_be_following'] && !following_sender?
  end

  def message?
    @notification.type == :mention
  end

  def direct_message?
    message? && @notification.target_status.direct_visibility?
  end

  # Returns true if the sender has been mentioned by the recipient up the thread
  def response_to_recipient?
    return false if @notification.target_status.in_reply_to_id.nil?

    # Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
    !Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id, depth_limit: 100]).zero?
      WITH RECURSIVE ancestors(id, in_reply_to_id, mention_id, path, depth) AS (
          SELECT s.id, s.in_reply_to_id, m.id, ARRAY[s.id], 0
          FROM statuses s
          LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
          WHERE s.id = :id
        UNION ALL
          SELECT s.id, s.in_reply_to_id, m.id, st.path || s.id, st.depth + 1
          FROM ancestors st
          JOIN statuses s ON s.id = st.in_reply_to_id
          LEFT JOIN mentions m ON m.silent = FALSE AND m.account_id = :sender_id AND m.status_id = s.id
          WHERE st.mention_id IS NULL AND NOT s.id = ANY(path) AND st.depth < :depth_limit
      )
      SELECT COUNT(*)
      FROM ancestors st
      JOIN statuses s ON s.id = st.id
      WHERE st.mention_id IS NOT NULL AND s.visibility = 3
    SQL
  end

  def from_staff?
    @notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user_role&.overrides?(@recipient.user_role)
  end

  def optional_non_following_and_direct?
    direct_message? &&
      @recipient.user.settings['interactions.must_be_following_dm'] &&
      !following_sender? &&
      !response_to_recipient?
  end

  def hellbanned?
    @notification.from_account.silenced? && !following_sender?
  end

  def from_self?
    @recipient.id == @notification.from_account.id
  end

  def domain_blocking?
    @recipient.domain_blocking?(@notification.from_account.domain) && !following_sender?
  end

  def blocked?
    blocked   = @recipient.unavailable?
    blocked ||= from_self? && @notification.type != :poll

    return blocked if message? && from_staff?

    blocked ||= domain_blocking?
    blocked ||= @recipient.blocking?(@notification.from_account)
    blocked ||= @recipient.muting_notifications?(@notification.from_account)
    blocked ||= hellbanned?
    blocked ||= optional_non_follower?
    blocked ||= optional_non_following?
    blocked ||= optional_non_following_and_direct?
    blocked ||= conversation_muted?
    blocked ||= blocked_mention? if @notification.type == :mention
    blocked
  end

  def conversation_muted?
    if @notification.target_status
      @recipient.muting_conversation?(@notification.target_status.conversation)
    else
      false
    end
  end

  def push_notification!
    push_to_streaming_api! if subscribed_to_streaming_api?
    push_to_web_push_subscriptions!
  end

  def push_to_streaming_api!
    redis.publish("timeline:#{@recipient.id}:notifications", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))
  end

  def subscribed_to_streaming_api?
    redis.exists?("subscribed:timeline:#{@recipient.id}") || redis.exists?("subscribed:timeline:#{@recipient.id}:notifications")
  end

  def push_to_conversation!
    AccountConversation.add_status(@recipient, @notification.target_status)
  end

  def push_to_web_push_subscriptions!
    ::Web::PushNotificationWorker.push_bulk(web_push_subscriptions.select { |subscription| subscription.pushable?(@notification) }) { |subscription| [subscription.id, @notification.id] }
  end

  def web_push_subscriptions
    @web_push_subscriptions ||= ::Web::PushSubscription.where(user_id: @recipient.user.id).to_a
  end

  def subscribed_to_web_push?
    web_push_subscriptions.any?
  end

  def send_email!
    return unless NotificationMailer.respond_to?(@notification.type)

    NotificationMailer
      .with(recipient: @recipient, notification: @notification)
      .public_send(@notification.type)
      .deliver_later(wait: 2.minutes)
  end

  def email_needed?
    (!recipient_online? || always_send_emails?) && send_email_for_notification_type?
  end

  def recipient_online?
    subscribed_to_streaming_api? || subscribed_to_web_push?
  end

  def always_send_emails?
    @recipient.user.settings.always_send_emails
  end

  def send_email_for_notification_type?
    NON_EMAIL_TYPES.exclude?(@notification.type) && @recipient.user.settings["notification_emails.#{@notification.type}"]
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe NotifyService, type: :service do
  subject { described_class.new.call(recipient, type, activity) }

  let(:user) { Fabricate(:user) }
  let(:recipient) { user.account }
  let(:sender) { Fabricate(:account, domain: 'example.com') }
  let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
  let(:type) { :follow }

  it { expect { subject }.to change(Notification, :count).by(1) }

  it 'does not notify when sender is blocked' do
    recipient.block!(sender)
    expect { subject }.to_not change(Notification, :count)
  end

  it 'does not notify when sender is muted with hide_notifications' do
    recipient.mute!(sender, notifications: true)
    expect { subject }.to_not change(Notification, :count)
  end

  it 'does notify when sender is muted without hide_notifications' do
    recipient.mute!(sender, notifications: false)
    expect { subject }.to change(Notification, :count)
  end

  it 'does not notify when sender\'s domain is blocked' do
    recipient.block_domain!(sender.domain)
    expect { subject }.to_not change(Notification, :count)
  end

  it 'does still notify when sender\'s domain is blocked but sender is followed' do
    recipient.block_domain!(sender.domain)
    recipient.follow!(sender)
    expect { subject }.to change(Notification, :count)
  end

  it 'does not notify when sender is silenced and not followed' do
    sender.silence!
    expect { subject }.to_not change(Notification, :count)
  end

  it 'does not notify when recipient is suspended' do
    recipient.suspend!
    expect { subject }.to_not change(Notification, :count)
  end

  context 'with direct messages' do
    let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) }
    let(:type)     { :mention }

    before do
      user.settings.update('interactions.must_be_following_dm': enabled)
      user.save
    end

    context 'when recipient is supposed to be following sender' do
      let(:enabled) { true }

      it 'does not notify' do
        expect { subject }.to_not change(Notification, :count)
      end

      context 'when the message chain is initiated by recipient, but is not direct message' do
        let(:reply_to) { Fabricate(:status, account: recipient) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }

        it 'does not notify' do
          expect { subject }.to_not change(Notification, :count)
        end
      end

      context 'when the message chain is initiated by recipient, but without a mention to the sender, even if the sender sends multiple messages in a row' do
        let(:reply_to) { Fabricate(:status, account: recipient) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:dummy_reply) { Fabricate(:status, account: sender, visibility: :direct, thread: reply_to) }
        let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: dummy_reply)) }

        it 'does not notify' do
          expect { subject }.to_not change(Notification, :count)
        end
      end

      context 'when the message chain is initiated by the recipient with a mention to the sender' do
        let(:reply_to) { Fabricate(:status, account: recipient, visibility: :direct) }
        let!(:mention) { Fabricate(:mention, account: sender, status: reply_to) }
        let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) }

        it 'does notify' do
          expect { subject }.to change(Notification, :count)
        end
      end
    end

    context 'when recipient is NOT supposed to be following sender' do
      let(:enabled) { false }

      it 'does notify' do
        expect { subject }.to change(Notification, :count)
      end
    end
  end

  describe 'reblogs' do
    let(:status)   { Fabricate(:status, account: Fabricate(:account)) }
    let(:activity) { Fabricate(:status, account: sender, reblog: status) }
    let(:type)     { :reblog }

    it 'shows reblogs by default' do
      recipient.follow!(sender)
      expect { subject }.to change(Notification, :count)
    end

    it 'shows reblogs when explicitly enabled' do
      recipient.follow!(sender, reblogs: true)
      expect { subject }.to change(Notification, :count)
    end

    it 'shows reblogs when disabled' do
      recipient.follow!(sender, reblogs: false)
      expect { subject }.to change(Notification, :count)
    end
  end

  context 'with muted and blocked users' do
    let(:asshole)  { Fabricate(:account, username: 'asshole') }
    let(:reply_to) { Fabricate(:status, account: asshole) }
    let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, thread: reply_to)) }
    let(:type)     { :mention }

    it 'does not notify when conversation is muted' do
      recipient.mute_conversation!(activity.status.conversation)
      expect { subject }.to_not change(Notification, :count)
    end

    it 'does not notify when it is a reply to a blocked user' do
      recipient.block!(asshole)
      expect { subject }.to_not change(Notification, :count)
    end
  end

  context 'with sender as recipient' do
    let(:sender) { recipient }

    it 'does not notify when recipient is the sender' do
      expect { subject }.to_not change(Notification, :count)
    end
  end

  describe 'email' do
    before do
      ActionMailer::Base.deliveries.clear

      user.settings.update('notification_emails.follow': enabled)
      user.save
    end

    context 'when email notification is enabled' do
      let(:enabled) { true }

      it 'sends email' do
        expect { subject }.to change(ActionMailer::Base.deliveries, :count).by(1)
      end
    end

    context 'when email notification is disabled' do
      let(:enabled) { false }

      it "doesn't send email" do
        expect { subject }.to_not change(ActionMailer::Base.deliveries, :count).from(0)
      end
    end
  end
end


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

class TranslateStatusService < BaseService
  CACHE_TTL = 1.day.freeze

  include ERB::Util
  include FormattingHelper

  def call(status, target_language)
    @status = status
    @source_texts = source_texts
    @target_language = target_language

    raise Mastodon::NotPermittedError unless permitted?

    status_translation = Rails.cache.fetch("v2:translations/#{@status.language}/#{@target_language}/#{content_hash}", expires_in: CACHE_TTL) do
      translations = translation_backend.translate(@source_texts.values, @status.language, @target_language)
      build_status_translation(translations)
    end

    status_translation.status = @status

    status_translation
  end

  private

  def translation_backend
    @translation_backend ||= TranslationService.configured
  end

  def permitted?
    return false unless @status.distributable? && TranslationService.configured?

    languages[@status.language]&.include?(@target_language)
  end

  def languages
    Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages }
  end

  def content_hash
    Digest::SHA256.base64digest(@source_texts.transform_keys { |key| key.respond_to?(:id) ? "#{key.class}-#{key.id}" : key }.to_json)
  end

  def source_texts
    texts = {}
    texts[:content] = wrap_emoji_shortcodes(status_content_format(@status)) if @status.content.present?
    texts[:spoiler_text] = wrap_emoji_shortcodes(html_escape(@status.spoiler_text)) if @status.spoiler_text.present?

    @status.preloadable_poll&.loaded_options&.each do |option|
      texts[option] = wrap_emoji_shortcodes(html_escape(option.title))
    end

    @status.media_attachments.each do |media_attachment|
      texts[media_attachment] = html_escape(media_attachment.description)
    end

    texts
  end

  def build_status_translation(translations)
    status_translation = Translation.new(
      detected_source_language: translations.first&.detected_source_language,
      language: @target_language,
      provider: translations.first&.provider,
      content: '',
      spoiler_text: '',
      poll_options: [],
      media_attachments: []
    )

    @source_texts.keys.each_with_index do |source, index|
      translation = translations[index]

      case source
      when :content
        node = unwrap_emoji_shortcodes(translation.text)
        Sanitize.node!(node, Sanitize::Config::MASTODON_STRICT)
        status_translation.content = node.to_html
      when :spoiler_text
        status_translation.spoiler_text = unwrap_emoji_shortcodes(translation.text).content
      when Poll::Option
        status_translation.poll_options << Translation::Option.new(
          title: unwrap_emoji_shortcodes(translation.text).content
        )
      when MediaAttachment
        status_translation.media_attachments << Translation::MediaAttachment.new(
          id: source.id,
          description: html_entities.decode(translation.text)
        )
      end
    end

    status_translation
  end

  def wrap_emoji_shortcodes(text)
    EmojiFormatter.new(text, @status.emojis, { raw_shortcode: true }).to_s
  end

  def unwrap_emoji_shortcodes(html)
    fragment = Nokogiri::HTML.fragment(html)
    fragment.css('span[translate="no"]').each do |element|
      element.remove_attribute('translate')
      element.replace(element.children) if element.attributes.empty?
    end
    fragment
  end

  def html_entities
    HTMLEntities.new
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe TranslateStatusService, type: :service do
  subject(:service) { described_class.new }

  let(:status) { Fabricate(:status, text: text, spoiler_text: spoiler_text, language: 'en', preloadable_poll: poll, media_attachments: media_attachments) }
  let(:text) { 'Hello' }
  let(:spoiler_text) { '' }
  let(:poll) { nil }
  let(:media_attachments) { [] }

  before do
    Fabricate(:custom_emoji, shortcode: 'highfive')
  end

  describe '#call' do
    before do
      translation_service = TranslationService.new
      allow(translation_service).to receive(:languages).and_return({ 'en' => ['es'] })
      allow(translation_service).to receive(:translate) do |texts|
        texts.map do |text|
          TranslationService::Translation.new(
            text: text.gsub('Hello', 'Hola').gsub('higfive', 'cincoaltos'),
            detected_source_language: 'en',
            provider: 'Dummy'
          )
        end
      end

      allow(TranslationService).to receive_messages(configured?: true, configured: translation_service)
    end

    it 'returns translated status content' do
      expect(service.call(status, 'es').content).to eq '<p>Hola</p>'
    end

    it 'returns source language' do
      expect(service.call(status, 'es').detected_source_language).to eq 'en'
    end

    it 'returns translation provider' do
      expect(service.call(status, 'es').provider).to eq 'Dummy'
    end

    it 'returns original status' do
      expect(service.call(status, 'es').status).to eq status
    end

    describe 'status has content with custom emoji' do
      let(:text) { 'Hello & :highfive:' }

      it 'does not translate shortcode' do
        expect(service.call(status, 'es').content).to eq '<p>Hola &amp; :highfive:</p>'
      end
    end

    describe 'status has no spoiler_text' do
      it 'returns an empty string' do
        expect(service.call(status, 'es').spoiler_text).to eq ''
      end
    end

    describe 'status has spoiler_text' do
      let(:spoiler_text) { 'Hello & Hello!' }

      it 'translates the spoiler text' do
        expect(service.call(status, 'es').spoiler_text).to eq 'Hola & Hola!'
      end
    end

    describe 'status has spoiler_text with custom emoji' do
      let(:spoiler_text) { 'Hello :highfive:' }

      it 'does not translate shortcode' do
        expect(service.call(status, 'es').spoiler_text).to eq 'Hola :highfive:'
      end
    end

    describe 'status has spoiler_text with unmatched custom emoji' do
      let(:spoiler_text) { 'Hello :Hello:' }

      it 'translates the invalid shortcode' do
        expect(service.call(status, 'es').spoiler_text).to eq 'Hola :Hola:'
      end
    end

    describe 'status has poll' do
      let(:poll) { Fabricate(:poll, options: ['Hello 1', 'Hello 2']) }

      it 'translates the poll option title' do
        status_translation = service.call(status, 'es')
        expect(status_translation.poll_options.size).to eq 2
        expect(status_translation.poll_options.first.title).to eq 'Hola 1'
      end
    end

    describe 'status has media attachment' do
      let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello & :highfive:')] }

      it 'translates the media attachment description' do
        status_translation = service.call(status, 'es')

        media_attachment = status_translation.media_attachments.first
        expect(media_attachment.id).to eq media_attachments.first.id
        expect(media_attachment.description).to eq 'Hola & :highfive:'
      end
    end
  end

  describe '#source_texts' do
    before do
      service.instance_variable_set(:@status, status)
    end

    describe 'status only has content' do
      it 'returns formatted content' do
        expect(service.send(:source_texts)).to eq({ content: '<p>Hello</p>' })
      end
    end

    describe 'status content contains custom emoji' do
      let(:status) { Fabricate(:status, text: 'Hello :highfive:') }

      it 'returns formatted content' do
        source_texts = service.send(:source_texts)
        expect(source_texts[:content]).to eq '<p>Hello <span translate="no">:highfive:</span></p>'
      end
    end

    describe 'status content contains tags' do
      let(:status) { Fabricate(:status, text: 'Hello #hola') }

      it 'returns formatted content' do
        source_texts = service.send(:source_texts)
        expect(source_texts[:content]).to include '<p>Hello <a'
        expect(source_texts[:content]).to include '/tags/hola'
      end
    end

    describe 'status has spoiler text' do
      let(:status) { Fabricate(:status, spoiler_text: 'Hello :highfive:') }

      it 'returns formatted spoiler text' do
        source_texts = service.send(:source_texts)
        expect(source_texts[:spoiler_text]).to eq 'Hello <span translate="no">:highfive:</span>'
      end
    end

    describe 'status has poll' do
      let(:poll) { Fabricate(:poll, options: %w(Blue Green)) }

      context 'with source texts from the service' do
        let!(:source_texts) { service.send(:source_texts) }

        it 'returns formatted poll options' do
          expect(source_texts.size).to eq 3
          expect(source_texts.values).to eq %w(<p>Hello</p> Blue Green)
        end

        it 'has a first key with content' do
          expect(source_texts.keys.first).to eq :content
        end

        it 'has the first option in the second key with correct options' do
          option1 = source_texts.keys.second
          expect(option1).to be_a Poll::Option
          expect(option1.id).to eq '0'
          expect(option1.title).to eq 'Blue'
        end

        it 'has the second option in the third key with correct options' do
          option2 = source_texts.keys.third
          expect(option2).to be_a Poll::Option
          expect(option2.id).to eq '1'
          expect(option2.title).to eq 'Green'
        end
      end
    end

    describe 'status has poll with custom emoji' do
      let(:poll) { Fabricate(:poll, options: ['Blue', 'Green :highfive:']) }

      it 'returns formatted poll options' do
        html = service.send(:source_texts).values.last
        expect(html).to eq 'Green <span translate="no">:highfive:</span>'
      end
    end

    describe 'status has media attachments' do
      let(:text) { '' }
      let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello :highfive:')] }

      it 'returns media attachments without custom emoji rendering' do
        source_texts = service.send(:source_texts)
        expect(source_texts.size).to eq 1

        key, text = source_texts.first
        expect(key).to eq media_attachments.first
        expect(text).to eq 'Hello :highfive:'
      end
    end
  end

  describe '#wrap_emoji_shortcodes' do
    before do
      service.instance_variable_set(:@status, status)
    end

    describe 'string contains custom emoji' do
      let(:text) { ':highfive:' }

      it 'renders the emoji' do
        html = service.send(:wrap_emoji_shortcodes, 'Hello :highfive:'.html_safe)
        expect(html).to eq 'Hello <span translate="no">:highfive:</span>'
      end
    end
  end

  describe '#unwrap_emoji_shortcodes' do
    describe 'string contains custom emoji' do
      it 'inserts the shortcode' do
        fragment = service.send(:unwrap_emoji_shortcodes, '<p>Hello <span translate="no">:highfive:</span>!</p>')
        expect(fragment.to_html).to eq '<p>Hello :highfive:!</p>'
      end

      it 'preserves other attributes than translate=no' do
        fragment = service.send(:unwrap_emoji_shortcodes, '<p>Hello <span translate="no" class="foo">:highfive:</span>!</p>')
        expect(fragment.to_html).to eq '<p>Hello <span class="foo">:highfive:</span>!</p>'
      end
    end
  end
end


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

class FanOutOnWriteService < BaseService
  include Redisable

  # Push a status into home and mentions feeds
  # @param [Status] status
  # @param [Hash] options
  # @option options [Boolean] update
  # @option options [Array<Integer>] silenced_account_ids
  # @option options [Boolean] skip_notifications
  def call(status, options = {})
    @status    = status
    @account   = status.account
    @options   = options

    check_race_condition!
    warm_payload_cache!

    fan_out_to_local_recipients!
    fan_out_to_public_recipients! if broadcastable?
    fan_out_to_public_streams! if broadcastable?
  end

  private

  def check_race_condition!
    # I don't know why but at some point we had an issue where
    # this service was being executed with status objects
    # that had a null visibility - which should not be possible
    # since the column in the database is not nullable.
    #
    # This check re-queues the service to be run at a later time
    # with the full object, if something like it occurs

    raise Mastodon::RaceConditionError if @status.visibility.nil?
  end

  def fan_out_to_local_recipients!
    deliver_to_self!

    unless @options[:skip_notifications]
      notify_mentioned_accounts!
      notify_about_update! if update?
    end

    case @status.visibility.to_sym
    when :public, :unlisted, :private
      deliver_to_all_followers!
      deliver_to_lists!
    when :limited
      deliver_to_mentioned_followers!
    else
      deliver_to_mentioned_followers!
      deliver_to_conversation!
    end
  end

  def fan_out_to_public_recipients!
    deliver_to_hashtag_followers!
  end

  def fan_out_to_public_streams!
    broadcast_to_hashtag_streams!
    broadcast_to_public_streams!
  end

  def deliver_to_self!
    FeedManager.instance.push_to_home(@account, @status, update: update?) if @account.local?
  end

  def notify_mentioned_accounts!
    @status.active_mentions.where.not(id: @options[:silenced_account_ids] || []).joins(:account).merge(Account.local).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
      LocalNotificationWorker.push_bulk(mentions) do |mention|
        [mention.account_id, mention.id, 'Mention', 'mention']
      end
    end
  end

  def notify_about_update!
    @status.reblogged_by_accounts.merge(Account.local).select(:id).reorder(nil).find_in_batches do |accounts|
      LocalNotificationWorker.push_bulk(accounts) do |account|
        [account.id, @status.id, 'Status', 'update']
      end
    end
  end

  def deliver_to_all_followers!
    @account.followers_for_local_distribution.select(:id).reorder(nil).find_in_batches do |followers|
      FeedInsertWorker.push_bulk(followers) do |follower|
        [@status.id, follower.id, 'home', { 'update' => update? }]
      end
    end
  end

  def deliver_to_hashtag_followers!
    TagFollow.where(tag_id: @status.tags.map(&:id)).select(:id, :account_id).reorder(nil).find_in_batches do |follows|
      FeedInsertWorker.push_bulk(follows) do |follow|
        [@status.id, follow.account_id, 'tags', { 'update' => update? }]
      end
    end
  end

  def deliver_to_lists!
    @account.lists_for_local_distribution.select(:id).reorder(nil).find_in_batches do |lists|
      FeedInsertWorker.push_bulk(lists) do |list|
        [@status.id, list.id, 'list', { 'update' => update? }]
      end
    end
  end

  def deliver_to_mentioned_followers!
    @status.mentions.joins(:account).merge(@account.followers_for_local_distribution).select(:id, :account_id).reorder(nil).find_in_batches do |mentions|
      FeedInsertWorker.push_bulk(mentions) do |mention|
        [@status.id, mention.account_id, 'home', { 'update' => update? }]
      end
    end
  end

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

  def broadcast_to_public_streams!
    return if @status.reply? && @status.in_reply_to_account_id != @account.id

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

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

  def deliver_to_conversation!
    AccountConversation.add_status(@account, @status) unless update?
  end

  def warm_payload_cache!
    Rails.cache.write("fan-out/#{@status.id}", rendered_status)
  end

  def anonymous_payload
    @anonymous_payload ||= Oj.dump(
      event: update? ? :'status.update' : :update,
      payload: rendered_status
    )
  end

  def rendered_status
    @rendered_status ||= InlineRenderer.render(@status, nil, :status)
  end

  def update?
    @options[:update]
  end

  def broadcastable?
    @status.public_visibility? && !@status.reblog? && !@account.silenced?
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  let(:last_active_at) { Time.now.utc }
  let(:status) { Fabricate(:status, account: alice, visibility: visibility, text: 'Hello @bob #hoge') }

  let!(:alice) { Fabricate(:user, current_sign_in_at: last_active_at).account }
  let!(:bob)   { Fabricate(:user, current_sign_in_at: last_active_at, account_attributes: { username: 'bob' }).account }
  let!(:tom)   { Fabricate(:user, current_sign_in_at: last_active_at).account }

  before do
    bob.follow!(alice)
    tom.follow!(alice)

    ProcessMentionsService.new.call(status)
    ProcessHashtagsService.new.call(status)

    allow(redis).to receive(:publish)

    subject.call(status)
  end

  def home_feed_of(account)
    HomeFeed.new(account).get(10).map(&:id)
  end

  context 'when status is public' do
    let(:visibility) { 'public' }

    it 'is added to the home feed of its author' do
      expect(home_feed_of(alice)).to include status.id
    end

    it 'is added to the home feed of a follower' do
      expect(home_feed_of(bob)).to include status.id
      expect(home_feed_of(tom)).to include status.id
    end

    it 'is broadcast to the hashtag stream' do
      expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything)
      expect(redis).to have_received(:publish).with('timeline:hashtag:hoge:local', anything)
    end

    it 'is broadcast to the public stream' do
      expect(redis).to have_received(:publish).with('timeline:public', anything)
      expect(redis).to have_received(:publish).with('timeline:public:local', anything)
    end
  end

  context 'when status is limited' do
    let(:visibility) { 'limited' }

    it 'is added to the home feed of its author' do
      expect(home_feed_of(alice)).to include status.id
    end

    it 'is added to the home feed of the mentioned follower' do
      expect(home_feed_of(bob)).to include status.id
    end

    it 'is not added to the home feed of the other follower' do
      expect(home_feed_of(tom)).to_not include status.id
    end

    it 'is not broadcast publicly' do
      expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
      expect(redis).to_not have_received(:publish).with('timeline:public', anything)
    end
  end

  context 'when status is private' do
    let(:visibility) { 'private' }

    it 'is added to the home feed of its author' do
      expect(home_feed_of(alice)).to include status.id
    end

    it 'is added to the home feed of a follower' do
      expect(home_feed_of(bob)).to include status.id
      expect(home_feed_of(tom)).to include status.id
    end

    it 'is not broadcast publicly' do
      expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
      expect(redis).to_not have_received(:publish).with('timeline:public', anything)
    end
  end

  context 'when status is direct' do
    let(:visibility) { 'direct' }

    it 'is added to the home feed of its author' do
      expect(home_feed_of(alice)).to include status.id
    end

    it 'is added to the home feed of the mentioned follower' do
      expect(home_feed_of(bob)).to include status.id
    end

    it 'is not added to the home feed of the other follower' do
      expect(home_feed_of(tom)).to_not include status.id
    end

    it 'is not broadcast publicly' do
      expect(redis).to_not have_received(:publish).with('timeline:hashtag:hoge', anything)
      expect(redis).to_not have_received(:publish).with('timeline:public', anything)
    end
  end
end


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

class ResolveURLService < BaseService
  include JsonLdHelper
  include Authorization

  USERNAME_STATUS_RE = %r{/@(?<username>#{Account::USERNAME_RE})/(?<status_id>[0-9]+)\Z}

  def call(url, on_behalf_of: nil)
    @url          = url
    @on_behalf_of = on_behalf_of

    if local_url?
      process_local_url
    elsif !fetched_resource.nil?
      process_url
    else
      process_url_from_db
    end
  end

  private

  def process_url
    if equals_or_includes_any?(type, ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES)
      ActivityPub::FetchRemoteActorService.new.call(resource_url, prefetched_body: body)
    elsif equals_or_includes_any?(type, ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
      status = FetchRemoteStatusService.new.call(resource_url, prefetched_body: body)
      authorize_with @on_behalf_of, status, :show? unless status.nil?
      status
    end
  end

  def process_url_from_db
    if [500, 502, 503, 504, nil].include?(fetch_resource_service.response_code)
      account = Account.find_by(uri: @url)
      return account unless account.nil?
    end

    return unless @on_behalf_of.present? && [401, 403, 404].include?(fetch_resource_service.response_code)

    # It may happen that the resource is a private toot, and thus not fetchable,
    # but we can return the toot if we already know about it.
    scope = Status.where(uri: @url)

    # We don't have an index on `url`, so try guessing the `uri` from `url`
    parsed_url = Addressable::URI.parse(@url)
    parsed_url.path.match(USERNAME_STATUS_RE) do |matched|
      parsed_url.path = "/users/#{matched[:username]}/statuses/#{matched[:status_id]}"
      scope = scope.or(Status.where(uri: parsed_url.to_s, url: @url))
    end

    status = scope.first

    authorize_with @on_behalf_of, status, :show? unless status.nil?
    status
  rescue Mastodon::NotPermittedError
    nil
  end

  def fetched_resource
    @fetched_resource ||= fetch_resource_service.call(@url)
  end

  def fetch_resource_service
    @fetch_resource_service ||= FetchResourceService.new
  end

  def resource_url
    fetched_resource.first
  end

  def body
    fetched_resource.second[:prefetched_body]
  end

  def type
    json_data['type']
  end

  def json_data
    @json_data ||= body_to_json(body)
  end

  def local_url?
    TagManager.instance.local_url?(@url)
  end

  def process_local_url
    recognized_params = Rails.application.routes.recognize_path(@url)

    case recognized_params[:controller]
    when 'statuses'
      return unless recognized_params[:action] == 'show'

      status = Status.find_by(id: recognized_params[:id])
      check_local_status(status)
    when 'accounts'
      return unless recognized_params[:action] == 'show'

      Account.find_local(recognized_params[:username])
    when 'home'
      return unless recognized_params[:action] == 'index' && recognized_params[:username_with_domain].present?

      if recognized_params[:any]&.match?(/\A[0-9]+\Z/)
        status = Status.find_by(id: recognized_params[:any])
        check_local_status(status)
      elsif recognized_params[:any].blank?
        username, domain = recognized_params[:username_with_domain].gsub(/\A@/, '').split('@')
        return unless username.present? && domain.present?

        Account.find_remote(username, domain)
      end
    end
  end

  def check_local_status(status)
    return if status.nil?

    authorize_with @on_behalf_of, status, :show?
    status
  rescue Mastodon::NotPermittedError
    nil
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  describe '#call' do
    it 'returns nil when there is no resource url' do
      url = 'http://example.com/missing-resource'
      Fabricate(:account, uri: url, domain: 'example.com')
      service = instance_double(FetchResourceService)

      allow(FetchResourceService).to receive(:new).and_return service
      allow(service).to receive(:response_code).and_return(404)
      allow(service).to receive(:call).with(url).and_return(nil)

      expect(subject.call(url)).to be_nil
    end

    it 'returns known account on temporary error' do
      url           = 'http://example.com/missing-resource'
      known_account = Fabricate(:account, uri: url, domain: 'example.com')
      service = instance_double(FetchResourceService)

      allow(FetchResourceService).to receive(:new).and_return service
      allow(service).to receive(:response_code).and_return(500)
      allow(service).to receive(:call).with(url).and_return(nil)

      expect(subject.call(url)).to eq known_account
    end

    context 'when searching for a remote private status' do
      let(:account)  { Fabricate(:account) }
      let(:poster)   { Fabricate(:account, domain: 'example.com') }
      let(:url)      { 'https://example.com/@foo/42' }
      let(:uri)      { 'https://example.com/users/foo/statuses/42' }
      let!(:status)  { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) }

      before do
        stub_request(:get, url).to_return(status: 404) if url.present?
        stub_request(:get, uri).to_return(status: 404)
      end

      context 'when the account follows the poster' do
        before do
          account.follow!(poster)
        end

        context 'when the status uses Mastodon-style URLs' do
          let(:url) { 'https://example.com/@foo/42' }
          let(:uri) { 'https://example.com/users/foo/statuses/42' }

          it 'returns status by url' do
            expect(subject.call(url, on_behalf_of: account)).to eq(status)
          end

          it 'returns status by uri' do
            expect(subject.call(uri, on_behalf_of: account)).to eq(status)
          end
        end

        context 'when the status uses pleroma-style URLs' do
          let(:url) { nil }
          let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' }

          it 'returns status by uri' do
            expect(subject.call(uri, on_behalf_of: account)).to eq(status)
          end
        end
      end

      context 'when the account does not follow the poster' do
        context 'when the status uses Mastodon-style URLs' do
          let(:url) { 'https://example.com/@foo/42' }
          let(:uri) { 'https://example.com/users/foo/statuses/42' }

          it 'does not return the status by url' do
            expect(subject.call(url, on_behalf_of: account)).to be_nil
          end

          it 'does not return the status by uri' do
            expect(subject.call(uri, on_behalf_of: account)).to be_nil
          end
        end

        context 'when the status uses pleroma-style URLs' do
          let(:url) { nil }
          let(:uri) { 'https://example.com/objects/0123-456-789-abc-def' }

          it 'returns status by uri' do
            expect(subject.call(uri, on_behalf_of: account)).to be_nil
          end
        end
      end
    end

    context 'when searching for a local private status' do
      let(:account) { Fabricate(:account) }
      let(:poster)  { Fabricate(:account) }
      let!(:status) { Fabricate(:status, account: poster, visibility: :private) }
      let(:url)     { ActivityPub::TagManager.instance.url_for(status) }
      let(:uri)     { ActivityPub::TagManager.instance.uri_for(status) }

      context 'when the account follows the poster' do
        before do
          account.follow!(poster)
        end

        it 'returns status by url' do
          expect(subject.call(url, on_behalf_of: account)).to eq(status)
        end

        it 'returns status by uri' do
          expect(subject.call(uri, on_behalf_of: account)).to eq(status)
        end
      end

      context 'when the account does not follow the poster' do
        it 'does not return the status by url' do
          expect(subject.call(url, on_behalf_of: account)).to be_nil
        end

        it 'does not return the status by uri' do
          expect(subject.call(uri, on_behalf_of: account)).to be_nil
        end
      end
    end

    context 'when searching for a link that redirects to a local public status' do
      let(:account) { Fabricate(:account) }
      let(:poster)  { Fabricate(:account) }
      let!(:status) { Fabricate(:status, account: poster, visibility: :public) }
      let(:url)     { 'https://link.to/foobar' }
      let(:status_url) { ActivityPub::TagManager.instance.url_for(status) }
      let(:uri) { ActivityPub::TagManager.instance.uri_for(status) }

      before do
        stub_request(:get, url).to_return(status: 302, headers: { 'Location' => status_url })
        body = ActiveModelSerializers::SerializableResource.new(status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter).to_json
        stub_request(:get, status_url).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })
      end

      it 'returns status by url' do
        expect(subject.call(url, on_behalf_of: account)).to eq(status)
      end
    end

    context 'when searching for a local link of a remote private status' do
      let(:account)    { Fabricate(:account) }
      let(:poster)     { Fabricate(:account, username: 'foo', domain: 'example.com') }
      let(:url)        { 'https://example.com/@foo/42' }
      let(:uri)        { 'https://example.com/users/foo/statuses/42' }
      let!(:status)    { Fabricate(:status, url: url, uri: uri, account: poster, visibility: :private) }
      let(:search_url) { "https://#{Rails.configuration.x.local_domain}/@foo@example.com/#{status.id}" }

      before do
        stub_request(:get, url).to_return(status: 404) if url.present?
        stub_request(:get, uri).to_return(status: 404)
      end

      context 'when the account follows the poster' do
        before do
          account.follow!(poster)
        end

        it 'returns the status' do
          expect(subject.call(search_url, on_behalf_of: account)).to eq(status)
        end
      end

      context 'when the account does not follow the poster' do
        it 'does not return the status' do
          expect(subject.call(search_url, on_behalf_of: account)).to be_nil
        end
      end
    end
  end
end


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

require 'zip'

class BackupService < BaseService
  include Payloadable
  include ContextHelper

  attr_reader :account, :backup

  def call(backup)
    @backup  = backup
    @account = backup.user.account

    build_archive!
  end

  private

  def build_outbox_json!(file)
    skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
    skeleton[:@context] = full_context
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')
    add_comma = false

    file.write(prepend)

    account.statuses.with_includes.reorder(nil).find_in_batches do |statuses|
      file.write(',') if add_comma
      add_comma = true

      file.write(statuses.map do |status|
        item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer)
        item.delete('@context')

        unless item[:type] == 'Announce' || item[:object][:attachment].blank?
          item[:object][:attachment].each do |attachment|
            attachment[:url] = Addressable::URI.parse(attachment[:url]).path.delete_prefix('/system/')
          end
        end

        Oj.dump(item)
      end.join(','))

      GC.start
    end

    file.write(append)
  end

  def build_archive!
    tmp_file = Tempfile.new(%w(archive .zip))

    Zip::File.open(tmp_file, create: true) do |zipfile|
      dump_outbox!(zipfile)
      dump_media_attachments!(zipfile)
      dump_likes!(zipfile)
      dump_bookmarks!(zipfile)
      dump_actor!(zipfile)
    end

    archive_filename = "#{['archive', Time.now.utc.strftime('%Y%m%d%H%M%S'), SecureRandom.hex(16)].join('-')}.zip"

    @backup.dump      = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file, filename: archive_filename)
    @backup.processed = true
    @backup.save!
  ensure
    tmp_file.close
    tmp_file.unlink
  end

  def dump_media_attachments!(zipfile)
    MediaAttachment.attached.where(account: account).find_in_batches do |media_attachments|
      media_attachments.each do |m|
        path = m.file&.path
        next unless path

        path = path.gsub(%r{\A.*/system/}, '')
        path = path.gsub(%r{\A/+}, '')
        download_to_zip(zipfile, m.file, path)
      end

      GC.start
    end
  end

  def dump_outbox!(zipfile)
    zipfile.get_output_stream('outbox.json') do |io|
      build_outbox_json!(io)
    end
  end

  def dump_actor!(zipfile)
    actor = serialize(account, ActivityPub::ActorSerializer)

    actor[:icon][:url]  = "avatar#{File.extname(actor[:icon][:url])}"  if actor[:icon]
    actor[:image][:url] = "header#{File.extname(actor[:image][:url])}" if actor[:image]
    actor[:outbox]      = 'outbox.json'
    actor[:likes]       = 'likes.json'
    actor[:bookmarks]   = 'bookmarks.json'

    download_to_zip(zipfile, account.avatar, "avatar#{File.extname(account.avatar.path)}") if account.avatar.exists?
    download_to_zip(zipfile, account.header, "header#{File.extname(account.header.path)}") if account.header.exists?

    json = Oj.dump(actor)

    zipfile.get_output_stream('actor.json') do |io|
      io.write(json)
    end
  end

  def dump_likes!(zipfile)
    skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'likes.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
    skeleton.delete(:totalItems)
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')

    zipfile.get_output_stream('likes.json') do |io|
      io.write(prepend)

      add_comma = false

      Status.reorder(nil).joins(:favourites).includes(:account).merge(account.favourites).find_in_batches do |statuses|
        io.write(',') if add_comma
        add_comma = true

        io.write(statuses.map do |status|
          Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
        end.join(','))

        GC.start
      end

      io.write(append)
    end
  end

  def dump_bookmarks!(zipfile)
    skeleton = serialize(ActivityPub::CollectionPresenter.new(id: 'bookmarks.json', type: :ordered, size: 0, items: []), ActivityPub::CollectionSerializer)
    skeleton.delete(:totalItems)
    skeleton[:orderedItems] = ['!PLACEHOLDER!']
    skeleton = Oj.dump(skeleton)
    prepend, append = skeleton.split('"!PLACEHOLDER!"')

    zipfile.get_output_stream('bookmarks.json') do |io|
      io.write(prepend)

      add_comma = false
      Status.reorder(nil).joins(:bookmarks).includes(:account).merge(account.bookmarks).find_in_batches do |statuses|
        io.write(',') if add_comma
        add_comma = true

        io.write(statuses.map do |status|
          Oj.dump(ActivityPub::TagManager.instance.uri_for(status))
        end.join(','))

        GC.start
      end

      io.write(append)
    end
  end

  def collection_presenter
    ActivityPub::CollectionPresenter.new(
      id: 'outbox.json',
      type: :ordered,
      size: account.statuses_count,
      items: []
    )
  end

  def serialize(object, serializer)
    ActiveModelSerializers::SerializableResource.new(
      object,
      serializer: serializer,
      adapter: ActivityPub::Adapter
    ).as_json
  end

  CHUNK_SIZE = 1.megabyte

  def download_to_zip(zipfile, attachment, filename)
    adapter = Paperclip.io_adapters.for(attachment)

    zipfile.get_output_stream(filename) do |io|
      while (buffer = adapter.read(CHUNK_SIZE))
        io.write(buffer)
      end
    end
  rescue Errno::ENOENT, Seahorse::Client::NetworkingError => e
    Rails.logger.warn "Could not backup file #{filename}: #{e}"
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe BackupService, type: :service do
  subject(:service_call) { described_class.new.call(backup) }

  let!(:user)           { Fabricate(:user) }
  let!(:attachment)     { Fabricate(:media_attachment, account: user.account) }
  let!(:status)         { Fabricate(:status, account: user.account, text: 'Hello', visibility: :public, media_attachments: [attachment]) }
  let!(:private_status) { Fabricate(:status, account: user.account, text: 'secret', visibility: :private) }
  let!(:favourite)      { Fabricate(:favourite, account: user.account) }
  let!(:bookmark)       { Fabricate(:bookmark, account: user.account) }
  let!(:backup)         { Fabricate(:backup, user: user) }

  def read_zip_file(backup, filename)
    file = Paperclip.io_adapters.for(backup.dump)
    Zip::File.open(file) do |zipfile|
      entry = zipfile.glob(filename).first
      return entry.get_input_stream.read
    end
  end

  context 'when the user has an avatar and header' do
    before do
      user.account.update!(avatar: attachment_fixture('avatar.gif'))
      user.account.update!(header: attachment_fixture('emojo.png'))
    end

    it 'stores them as expected' do
      service_call

      json = export_json(:actor)
      avatar_path = json.dig('icon', 'url')
      header_path = json.dig('image', 'url')

      expect(avatar_path).to_not be_nil
      expect(header_path).to_not be_nil

      expect(read_zip_file(backup, avatar_path)).to be_present
      expect(read_zip_file(backup, header_path)).to be_present
    end
  end

  it 'marks the backup as processed and exports files' do
    expect { service_call }.to process_backup

    expect_outbox_export
    expect_likes_export
    expect_bookmarks_export
  end

  def process_backup
    change(backup, :processed).from(false).to(true)
  end

  def expect_outbox_export
    json = export_json(:outbox)

    aggregate_failures do
      expect(json['@context']).to_not be_nil
      expect(json['type']).to eq 'OrderedCollection'
      expect(json['totalItems']).to eq 2
      expect(json['orderedItems'][0]['@context']).to be_nil
      expect(json['orderedItems'][0]).to include_create_item(status)
      expect(json['orderedItems'][1]).to include_create_item(private_status)
    end
  end

  def expect_likes_export
    json = export_json(:likes)

    aggregate_failures do
      expect(json['type']).to eq 'OrderedCollection'
      expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(favourite.status)]
    end
  end

  def expect_bookmarks_export
    json = export_json(:bookmarks)

    aggregate_failures do
      expect(json['type']).to eq 'OrderedCollection'
      expect(json['orderedItems']).to eq [ActivityPub::TagManager.instance.uri_for(bookmark.status)]
    end
  end

  def export_json(type)
    Oj.load(read_zip_file(backup, "#{type}.json"))
  end

  def include_create_item(status)
    include({
      'type' => 'Create',
      'object' => include({
        'id' => ActivityPub::TagManager.instance.uri_for(status),
        'content' => "<p>#{status.text}</p>",
      }),
    })
  end
end


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

class AccountStatusesCleanupService < BaseService
  # @param [AccountStatusesCleanupPolicy] account_policy
  # @param [Integer] budget
  # @return [Integer]
  def call(account_policy, budget = 50)
    return 0 unless account_policy.enabled?

    cutoff_id = account_policy.compute_cutoff_id
    return 0 if cutoff_id.blank?

    num_deleted = 0
    last_deleted = nil

    account_policy.statuses_to_delete(budget, cutoff_id, account_policy.last_inspected).reorder(nil).find_each(order: :asc) do |status|
      status.discard_with_reblogs
      RemovalWorker.perform_async(status.id, { 'redraft' => false })
      num_deleted += 1
      last_deleted = status.id
    end

    account_policy.record_last_inspected(last_deleted.presence || cutoff_id)

    num_deleted
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

describe AccountStatusesCleanupService, type: :service do
  let(:account)           { Fabricate(:account, username: 'alice', domain: nil) }
  let(:account_policy)    { Fabricate(:account_statuses_cleanup_policy, account: account) }
  let!(:unrelated_status) { Fabricate(:status, created_at: 3.years.ago) }

  describe '#call' do
    context 'when the account has not posted anything' do
      it 'returns 0 deleted toots' do
        expect(subject.call(account_policy)).to eq 0
      end
    end

    context 'when the account has posted several old statuses' do
      let!(:very_old_status)    { Fabricate(:status, created_at: 3.years.ago, account: account) }
      let!(:old_status)         { Fabricate(:status, created_at: 1.year.ago, account: account) }
      let!(:another_old_status) { Fabricate(:status, created_at: 1.year.ago, account: account) }
      let!(:recent_status)      { Fabricate(:status, created_at: 1.day.ago, account: account) }

      context 'when given a budget of 1' do
        it 'reports 1 deleted toot' do
          expect(subject.call(account_policy, 1)).to eq 1
        end
      end

      context 'when given a normal budget of 10' do
        it 'reports 3 deleted statuses' do
          expect(subject.call(account_policy, 10)).to eq 3
        end

        it 'records the last deleted id' do
          subject.call(account_policy, 10)
          expect(account_policy.last_inspected).to eq [old_status.id, another_old_status.id].max
        end

        it 'actually deletes the statuses' do
          subject.call(account_policy, 10)
          expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil
        end
      end

      context 'when called repeatedly with a budget of 2' do
        it 'reports 2 then 1 deleted statuses' do
          expect(subject.call(account_policy, 2)).to eq 2
          expect(subject.call(account_policy, 2)).to eq 1
        end

        it 'actually deletes the statuses in the expected order' do
          subject.call(account_policy, 2)
          expect(Status.find_by(id: very_old_status.id)).to be_nil
          subject.call(account_policy, 2)
          expect(Status.find_by(id: [very_old_status.id, old_status.id, another_old_status.id])).to be_nil
        end
      end

      context 'when a self-faved toot is unfaved' do
        let!(:self_faved) { Fabricate(:status, created_at: 6.months.ago, account: account) }
        let!(:favourite)  { Fabricate(:favourite, account: account, status: self_faved) }

        it 'deletes it once unfaved' do
          expect(subject.call(account_policy, 20)).to eq 3
          expect(Status.find_by(id: self_faved.id)).to_not be_nil
          expect(subject.call(account_policy, 20)).to eq 0
          favourite.destroy!
          expect(subject.call(account_policy, 20)).to eq 1
          expect(Status.find_by(id: self_faved.id)).to be_nil
        end
      end

      context 'when there are more un-deletable old toots than the early search cutoff' do
        before do
          stub_const 'AccountStatusesCleanupPolicy::EARLY_SEARCH_CUTOFF', 5
          # Old statuses that should be cut-off
          10.times do
            Fabricate(:status, created_at: 4.years.ago, visibility: :direct, account: account)
          end
          # New statuses that prevent cut-off id to reach the last status
          10.times do
            Fabricate(:status, created_at: 4.seconds.ago, visibility: :direct, account: account)
          end
        end

        it 'reports 0 deleted statuses then 0 then 3 then 0 again' do
          expect(subject.call(account_policy, 10)).to eq 0
          expect(subject.call(account_policy, 10)).to eq 0
          expect(subject.call(account_policy, 10)).to eq 3
          expect(subject.call(account_policy, 10)).to eq 0
        end

        it 'never causes the recorded id to get higher than oldest deletable toot' do
          subject.call(account_policy, 10)
          subject.call(account_policy, 10)
          subject.call(account_policy, 10)
          subject.call(account_policy, 10)
          expect(account_policy.last_inspected).to be < Mastodon::Snowflake.id_at(account_policy.min_status_age.seconds.ago, with_random: false)
        end
      end
    end
  end
end


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

class FetchOEmbedService
  ENDPOINT_CACHE_EXPIRES_IN = 24.hours.freeze
  URL_REGEX                 = %r{(=(https?(%3A|:)(//|%2F%2F)))([^&]*)}i

  attr_reader :url, :options, :format, :endpoint_url

  def call(url, options = {})
    @url     = url
    @options = options

    if @options[:cached_endpoint]
      parse_cached_endpoint!
    else
      discover_endpoint!
    end

    fetch!
  end

  private

  def discover_endpoint!
    return if html.nil?

    @format = @options[:format]
    page    = Nokogiri::HTML(html)

    if @format.nil? || @format == :json
      @endpoint_url ||= page.at_xpath('//link[@type="application/json+oembed"]|//link[@type="text/json+oembed"]')&.attribute('href')&.value
      @format       ||= :json if @endpoint_url
    end

    if @format.nil? || @format == :xml
      @endpoint_url ||= page.at_xpath('//link[@type="text/xml+oembed"]')&.attribute('href')&.value
      @format       ||= :xml if @endpoint_url
    end

    return if @endpoint_url.blank?

    @endpoint_url = begin
      base_url = Addressable::URI.parse(@url)

      # If the OEmbed endpoint is given as http but the URL we opened
      # was served over https, we can assume OEmbed will be available
      # through https as well

      (base_url + @endpoint_url).tap do |absolute_url|
        absolute_url.scheme = base_url.scheme if base_url.scheme == 'https'
      end.to_s
    end

    cache_endpoint!
  rescue Addressable::URI::InvalidURIError
    @endpoint_url = nil
  end

  def parse_cached_endpoint!
    cached = @options[:cached_endpoint]

    return if cached[:endpoint].nil? || cached[:format].nil?

    @endpoint_url = Addressable::Template.new(cached[:endpoint]).expand(url: @url).to_s
    @format       = cached[:format]
  end

  def cache_endpoint!
    return unless URL_REGEX.match?(@endpoint_url)

    url_domain = Addressable::URI.parse(@url).normalized_host

    endpoint_hash = {
      endpoint: @endpoint_url.gsub(URL_REGEX, '={url}'),
      format: @format,
    }

    Rails.cache.write("oembed_endpoint:#{url_domain}", endpoint_hash, expires_in: ENDPOINT_CACHE_EXPIRES_IN)
  end

  def fetch!
    return if @endpoint_url.blank?

    body = Request.new(:get, @endpoint_url).perform do |res|
      res.code == 200 ? res.body_with_limit : nil
    end

    validate(parse_for_format(body)) if body.present?
  rescue Oj::ParseError, Ox::ParseError
    nil
  end

  def parse_for_format(body)
    case @format
    when :json
      Oj.load(body, mode: :strict)&.with_indifferent_access
    when :xml
      Ox.load(body, mode: :hash_no_attrs)&.with_indifferent_access&.dig(:oembed)
    end
  end

  def validate(oembed)
    oembed if oembed[:version].to_s == '1.0' && oembed[:type].present?
  end

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

    @html = @options[:html] || Request.new(:get, @url).add_headers('Accept' => 'text/html').perform do |res|
      res.code != 200 || res.mime_type != 'text/html' ? nil : res.body_with_limit
    end
  end
end

```

Response:
# frozen_string_literal: true

require 'rails_helper'

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

  before do
    stub_request(:get, 'https://host.test/provider.json').to_return(status: 404)
    stub_request(:get, 'https://host.test/provider.xml').to_return(status: 404)
    stub_request(:get, 'https://host.test/empty_provider.json').to_return(status: 200)
  end

  describe 'discover_provider' do
    context 'when status code is 200 and MIME type is text/html' do
      context 'when OEmbed endpoint contains URL as parameter' do
        before do
          stub_request(:get, 'https://www.youtube.com/watch?v=IPSbNdBmWKE').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_youtube.html')
          )
          stub_request(:get, 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_json_empty.html')
          )
        end

        it 'returns new OEmbed::Provider for JSON provider' do
          subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE')
          expect(subject.endpoint_url).to eq 'https://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIPSbNdBmWKE'
          expect(subject.format).to eq :json
        end

        it 'stores URL template' do
          subject.call('https://www.youtube.com/watch?v=IPSbNdBmWKE')
          expect(Rails.cache.read('oembed_endpoint:www.youtube.com')[:endpoint]).to eq 'https://www.youtube.com/oembed?format=json&url={url}'
        end
      end

      context 'when both of JSON and XML provider are discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_json_xml.html')
          )
        end

        it 'returns new OEmbed::Provider for JSON provider if :format option is set to :json' do
          subject.call('https://host.test/oembed.html', format: :json)
          expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
          expect(subject.format).to eq :json
        end

        it 'returns new OEmbed::Provider for XML provider if :format option is set to :xml' do
          subject.call('https://host.test/oembed.html', format: :xml)
          expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
          expect(subject.format).to eq :xml
        end

        it 'does not cache OEmbed endpoint' do
          subject.call('https://host.test/oembed.html', format: :xml)
          expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false
        end
      end

      context 'when JSON provider is discoverable while XML provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_json.html')
          )
        end

        it 'returns new OEmbed::Provider for JSON provider' do
          subject.call('https://host.test/oembed.html')
          expect(subject.endpoint_url).to eq 'https://host.test/provider.json'
          expect(subject.format).to eq :json
        end

        it 'does not cache OEmbed endpoint' do
          subject.call('https://host.test/oembed.html')
          expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false
        end
      end

      context 'when XML provider is discoverable while JSON provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_xml.html')
          )
        end

        it 'returns new OEmbed::Provider for XML provider' do
          subject.call('https://host.test/oembed.html')
          expect(subject.endpoint_url).to eq 'https://host.test/provider.xml'
          expect(subject.format).to eq :xml
        end

        it 'does not cache OEmbed endpoint' do
          subject.call('https://host.test/oembed.html')
          expect(Rails.cache.exist?('oembed_endpoint:host.test')).to be false
        end
      end

      context 'with Invalid XML provider is discoverable while JSON provider is not' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_invalid_xml.html')
          )
        end

        it 'returns nil' do
          expect(subject.call('https://host.test/oembed.html')).to be_nil
        end
      end

      context 'with neither of JSON and XML provider is discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_undiscoverable.html')
          )
        end

        it 'returns nil' do
          expect(subject.call('https://host.test/oembed.html')).to be_nil
        end
      end

      context 'when empty JSON provider is discoverable' do
        before do
          stub_request(:get, 'https://host.test/oembed.html').to_return(
            status: 200,
            headers: { 'Content-Type': 'text/html' },
            body: request_fixture('oembed_json_empty.html')
          )
        end

        it 'returns new OEmbed::Provider for JSON provider' do
          subject.call('https://host.test/oembed.html')
          expect(subject.endpoint_url).to eq 'https://host.test/empty_provider.json'
          expect(subject.format).to eq :json
        end
      end
    end

    context 'when endpoint is cached' do
      before do
        stub_request(:get, 'http://www.youtube.com/oembed?format=json&url=https://www.youtube.com/watch?v=dqwpQarrDwk').to_return(
          status: 200,
          headers: { 'Content-Type': 'text/html' },
          body: request_fixture('oembed_json_empty.html')
        )
      end

      it 'returns new provider without fetching original URL first' do
        subject.call('https://www.youtube.com/watch?v=dqwpQarrDwk', cached_endpoint: { endpoint: 'http://www.youtube.com/oembed?format=json&url={url}', format: :json })
        expect(a_request(:get, 'https://www.youtube.com/watch?v=dqwpQarrDwk')).to_not have_been_made
        expect(subject.endpoint_url).to eq 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk'
        expect(subject.format).to eq :json
        expect(a_request(:get, 'http://www.youtube.com/oembed?format=json&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdqwpQarrDwk')).to have_been_made
      end
    end

    context 'when status code is not 200' do
      before do
        stub_request(:get, 'https://host.test/oembed.html').to_return(
          status: 400,
          headers: { 'Content-Type': 'text/html' },
          body: request_fixture('oembed_xml.html')
        )
      end

      it 'returns nil' do
        expect(subject.call('https://host.test/oembed.html')).to be_nil
      end
    end

    context 'when MIME type is not text/html' do
      before do
        stub_request(:get, 'https://host.test/oembed.html').to_return(
          status: 200,
          body: request_fixture('oembed_xml.html')
        )
      end

      it 'returns nil' do
        expect(subject.call('https://host.test/oembed.html')).to be_nil
      end
    end
  end
end