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

class DestroyTask
  def initialize(io = STDOUT)
    @io = io
  end

  def destroy_topics(category, parent_category = nil, delete_system_topics = false)
    c = Category.find_by_slug(category, parent_category)
    descriptive_slug = parent_category ? "#{parent_category}/#{category}" : category
    return @io.puts "A category with the slug: #{descriptive_slug} could not be found" if c.nil?
    if delete_system_topics
      topics = Topic.where(category_id: c.id, pinned_at: nil)
    else
      topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1)
    end
    @io.puts "There are #{topics.count} topics to delete in #{descriptive_slug} category"
    topics.find_each do |topic|
      @io.puts "Deleting #{topic.slug}..."
      first_post = topic.ordered_posts.first
      return @io.puts "Topic.ordered_posts.first was nil" if first_post.nil?
      @io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy
    end
  end

  def destroy_topics_in_category(category_id, delete_system_topics = false)
    c = Category.find(category_id)
    return @io.puts "A category with the id: #{category_id} could not be found" if c.nil?
    if delete_system_topics
      topics = Topic.where(category_id: c.id, pinned_at: nil)
    else
      topics = Topic.where(category_id: c.id, pinned_at: nil).where.not(user_id: -1)
    end
    @io.puts "There are #{topics.count} topics to delete in #{c.slug} category"
    topics.find_each do |topic|
      first_post = topic.ordered_posts.first
      return @io.puts "Topic.ordered_posts.first was nil for topic: #{topic.id}" if first_post.nil?
      PostDestroyer.new(Discourse.system_user, first_post).destroy
    end
    topics = Topic.where(category_id: c.id, pinned_at: nil)
    @io.puts "There are #{topics.count} topics that could not be deleted in #{c.slug} category"
  end

  def destroy_topics_all_categories
    categories = Category.all
    categories.each { |c| @io.puts destroy_topics(c.slug, c.parent_category&.slug) }
  end

  def destroy_private_messages
    Topic
      .where(archetype: "private_message")
      .find_each do |pm|
        @io.puts "Destroying #{pm.slug} pm"
        first_post = pm.ordered_posts.first
        @io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy
      end
  end

  def destroy_category(category_id, destroy_system_topics = false)
    c = Category.find_by_id(category_id)
    return @io.puts "A category with the id: #{category_id} could not be found" if c.nil?
    subcategories = Category.where(parent_category_id: c.id)
    @io.puts "There are #{subcategories.count} subcategories to delete" if subcategories
    subcategories.each { |s| category_topic_destroyer(s, destroy_system_topics) }
    category_topic_destroyer(c, destroy_system_topics)
  end

  def destroy_groups
    groups = Group.where(automatic: false)
    groups.each do |group|
      @io.puts "destroying group: #{group.id}"
      @io.puts group.destroy
    end
  end

  def destroy_users
    User
      .human_users
      .where(admin: false)
      .find_each do |user|
        begin
          if UserDestroyer.new(Discourse.system_user).destroy(
               user,
               delete_posts: true,
               context: "destroy task",
             )
            @io.puts "#{user.username} deleted"
          else
            @io.puts "#{user.username} not deleted"
          end
        rescue UserDestroyer::PostsExistError
          raise Discourse::InvalidAccess.new(
                  "User #{user.username} has #{user.post_count} posts, so can't be deleted.",
                )
        rescue NoMethodError
          @io.puts "#{user.username} could not be deleted"
        rescue Discourse::InvalidAccess => e
          @io.puts "#{user.username} #{e.message}"
        end
      end
  end

  def destroy_stats
    ApplicationRequest.delete_all
    IncomingLink.delete_all
    UserVisit.delete_all
    UserProfileView.delete_all
    UserProfile.update_all(views: 0)
    PostAction.unscoped.delete_all
    EmailLog.delete_all
  end

  private

  def category_topic_destroyer(category, destroy_system_topics = false)
    destroy_topics_in_category(category.id, destroy_system_topics)
    @io.puts "Destroying #{category.slug} category"
    category.destroy
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe DestroyTask do
  describe "destroy topics" do
    fab!(:c) { Fabricate(:category_with_definition) }
    fab!(:t) { Fabricate(:topic, category: c) }
    let!(:p) { Fabricate(:post, topic: t) }
    fab!(:c2) { Fabricate(:category_with_definition) }
    fab!(:t2) { Fabricate(:topic, category: c2) }
    let!(:p2) { Fabricate(:post, topic: t2) }
    fab!(:sc) { Fabricate(:category_with_definition, parent_category: c2) }
    fab!(:t3) { Fabricate(:topic, category: sc) }
    let!(:p3) { Fabricate(:post, topic: t3) }

    it "destroys all topics in a category" do
      destroy_task = DestroyTask.new(StringIO.new)
      expect { destroy_task.destroy_topics(c.slug) }.to change {
        Topic.where(category_id: c.id).count
      }.by (-1)
    end

    it "destroys all topics in a sub category" do
      destroy_task = DestroyTask.new(StringIO.new)
      expect { destroy_task.destroy_topics(sc.slug, c2.slug) }.to change {
        Topic.where(category_id: sc.id).count
      }.by(-1)
    end

    it "doesn't destroy system topics" do
      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_topics(c2.slug)
      expect(Topic.where(category_id: c2.id).count).to eq 1
    end

    it "destroys topics in all categories" do
      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_topics_all_categories
      expect(Post.where(topic_id: [t.id, t2.id, t3.id]).count).to eq 0
    end
  end

  describe "destroy categories" do
    fab!(:c) { Fabricate(:category_with_definition) }
    fab!(:t) { Fabricate(:topic, category: c) }
    let!(:p) { Fabricate(:post, topic: t) }
    fab!(:c2) { Fabricate(:category_with_definition) }
    fab!(:t2) { Fabricate(:topic, category: c) }
    let!(:p2) { Fabricate(:post, topic: t2) }
    fab!(:sc) { Fabricate(:category_with_definition, parent_category: c2) }
    fab!(:t3) { Fabricate(:topic, category: sc) }
    let!(:p3) { Fabricate(:post, topic: t3) }

    it "destroys specified category" do
      destroy_task = DestroyTask.new(StringIO.new)

      expect { destroy_task.destroy_category(c.id) }.to change {
        Category.where(id: c.id).count
      }.by (-1)
    end

    it "destroys sub-categories when destroying parent category" do
      destroy_task = DestroyTask.new(StringIO.new)

      expect { destroy_task.destroy_category(c2.id) }.to change {
        Category.where(id: sc.id).count
      }.by (-1)
    end
  end

  describe "private messages" do
    let!(:pm) { Fabricate(:private_message_post) }
    let!(:pm2) { Fabricate(:private_message_post) }

    it "destroys all private messages" do
      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_private_messages
      expect(Topic.where(archetype: "private_message").count).to eq 0
    end
  end

  describe "groups" do
    let!(:g) { Fabricate(:group) }
    let!(:g2) { Fabricate(:group) }

    it "destroys all groups" do
      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_groups
      expect(Group.where(automatic: false).count).to eq 0
    end

    it "doesn't destroy default groups" do
      destroy_task = DestroyTask.new(StringIO.new)
      before_count = Group.count
      destroy_task.destroy_groups
      expect(Group.count).to eq before_count - 2
    end
  end

  describe "users" do
    it "destroys all non-admin users" do
      before_count = User.count

      Fabricate(:user)
      Fabricate(:user)
      Fabricate(:admin)

      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_users
      expect(User.where(admin: false).count).to eq 0
      # admin does not get destroyed
      expect(User.count).to eq before_count + 1
    end
  end

  describe "stats" do
    it "destroys all site stats" do
      destroy_task = DestroyTask.new(StringIO.new)
      destroy_task.destroy_stats
    end
  end
end


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

require "net/imap"
require "net/smtp"
require "net/pop"

class EmailSettingsExceptionHandler
  EXPECTED_EXCEPTIONS = [
    Net::POPAuthenticationError,
    Net::IMAP::NoResponseError,
    Net::IMAP::Error,
    Net::SMTPAuthenticationError,
    Net::SMTPServerBusy,
    Net::SMTPSyntaxError,
    Net::SMTPFatalError,
    Net::SMTPUnknownError,
    Net::OpenTimeout,
    Net::ReadTimeout,
    SocketError,
    Errno::ECONNREFUSED,
  ]

  class GenericProvider
    def initialize(exception)
      @exception = exception
    end

    def message
      case @exception
      when Net::POPAuthenticationError
        net_pop_authentication_error
      when Net::IMAP::NoResponseError
        net_imap_no_response_error
      when Net::IMAP::Error
        net_imap_unhandled_error
      when Net::SMTPAuthenticationError
        net_smtp_authentication_error
      when Net::SMTPServerBusy
        net_smtp_server_busy
      when Net::SMTPSyntaxError, Net::SMTPFatalError, Net::SMTPUnknownError
        net_smtp_unhandled_error
      when SocketError, Errno::ECONNREFUSED
        socket_connection_error
      when Net::OpenTimeout, Net::ReadTimeout
        net_timeout_error
      else
        unhandled_error
      end
    end

    private

    def net_pop_authentication_error
      I18n.t("email_settings.pop3_authentication_error")
    end

    def net_imap_no_response_error
      # Most of IMAP's errors are lumped under the NoResponseError, including invalid
      # credentials errors, because it is raised when a "NO" response is
      # raised from the IMAP server https://datatracker.ietf.org/doc/html/rfc3501#section-7.1.2
      #
      # Generally, it should be fairly safe to just return the error message as is.
      if @exception.message.match(/Invalid credentials/)
        I18n.t("email_settings.imap_authentication_error")
      else
        I18n.t(
          "email_settings.imap_no_response_error",
          message: @exception.message.gsub(" (Failure)", ""),
        )
      end
    end

    def net_imap_unhandled_error
      I18n.t("email_settings.imap_unhandled_error", message: @exception.message)
    end

    def net_smtp_authentication_error
      I18n.t("email_settings.smtp_authentication_error")
    end

    def net_smtp_server_busy
      I18n.t("email_settings.smtp_server_busy_error")
    end

    def net_smtp_unhandled_error
      I18n.t("email_settings.smtp_unhandled_error", message: @exception.message)
    end

    def socket_connection_error
      I18n.t("email_settings.connection_error")
    end

    def net_timeout_error
      I18n.t("email_settings.timeout_error")
    end

    def unhandled_error
      I18n.t("email_settings.unhandled_error", message: @exception.message)
    end
  end

  class GmailProvider < GenericProvider
    def net_smtp_authentication_error
      # Gmail requires use of application-specific passwords when 2FA is enabled and return
      # a special error message calling this out.
      if @exception.message.match(/Application-specific password required/)
        I18n.t("email_settings.authentication_error_gmail_app_password")
      else
        super
      end
    end

    def net_imap_no_response_error
      # Gmail requires use of application-specific passwords when 2FA is enabled and return
      # a special error message calling this out.
      if @exception.message.match(/Application-specific password required/)
        I18n.t("email_settings.authentication_error_gmail_app_password")
      else
        super
      end
    end
  end

  def self.friendly_exception_message(exception, host)
    if host.include?("gmail.com")
      EmailSettingsExceptionHandler::GmailProvider.new(exception).message
    else
      EmailSettingsExceptionHandler::GenericProvider.new(exception).message
    end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe EmailSettingsExceptionHandler do
  describe "#friendly_exception_message" do
    it "formats a Net::POPAuthenticationError" do
      exception = Net::POPAuthenticationError.new("invalid credentials")
      expect(described_class.friendly_exception_message(exception, "pop.test.com")).to eq(
        I18n.t("email_settings.pop3_authentication_error"),
      )
    end

    it "formats a Net::IMAP::NoResponseError for invalid credentials" do
      exception = Net::IMAP::NoResponseError.new(stub(data: stub(text: "Invalid credentials")))
      expect(described_class.friendly_exception_message(exception, "imap.test.com")).to eq(
        I18n.t("email_settings.imap_authentication_error"),
      )
    end

    it "formats a general Net::IMAP::NoResponseError" do
      exception = Net::IMAP::NoResponseError.new(stub(data: stub(text: "NO bad problem (Failure)")))
      expect(described_class.friendly_exception_message(exception, "imap.test.com")).to eq(
        I18n.t("email_settings.imap_no_response_error", message: "NO bad problem"),
      )
    end

    it "formats a general Net::IMAP::NoResponseError with application-specific password Gmail error" do
      exception =
        Net::IMAP::NoResponseError.new(
          stub(data: stub(text: "NO Application-specific password required")),
        )
      expect(described_class.friendly_exception_message(exception, "imap.gmail.com")).to eq(
        I18n.t("email_settings.authentication_error_gmail_app_password"),
      )
    end

    it "formats a Net::SMTPAuthenticationError" do
      exception = Net::SMTPAuthenticationError.new("invalid credentials")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.smtp_authentication_error"),
      )
    end

    it "formats a Net::SMTPAuthenticationError with application-specific password Gmail error" do
      exception =
        Net::SMTPAuthenticationError.new(nil, message: "Application-specific password required")
      expect(described_class.friendly_exception_message(exception, "smtp.gmail.com")).to eq(
        I18n.t("email_settings.authentication_error_gmail_app_password"),
      )
    end

    it "formats a Net::SMTPServerBusy" do
      exception = Net::SMTPServerBusy.new("call me maybe later")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.smtp_server_busy_error"),
      )
    end

    it "formats a Net::SMTPSyntaxError, Net::SMTPFatalError, and Net::SMTPUnknownError" do
      exception = Net::SMTPSyntaxError.new(nil, message: "bad syntax")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.smtp_unhandled_error", message: exception.message),
      )
      exception = Net::SMTPFatalError.new(nil, message: "fatal")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.smtp_unhandled_error", message: exception.message),
      )
      exception = Net::SMTPUnknownError.new(nil, message: "unknown")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.smtp_unhandled_error", message: exception.message),
      )
    end

    it "formats a SocketError and Errno::ECONNREFUSED" do
      exception = SocketError.new("bad socket")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.connection_error"),
      )
      exception = Errno::ECONNREFUSED.new("no thanks")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.connection_error"),
      )
    end

    it "formats a Net::OpenTimeout and Net::ReadTimeout error" do
      exception = Net::OpenTimeout.new("timed out")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.timeout_error"),
      )
      exception = Net::ReadTimeout.new("timed out")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.timeout_error"),
      )
    end

    it "formats unhandled errors" do
      exception = StandardError.new("unknown")
      expect(described_class.friendly_exception_message(exception, "smtp.test.com")).to eq(
        I18n.t("email_settings.unhandled_error", message: exception.message),
      )
    end
  end
end


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

class PostOwnerChanger
  def initialize(params)
    @post_ids = params[:post_ids]
    @topic = Topic.with_deleted.find_by(id: params[:topic_id].to_i)
    @new_owner = params[:new_owner]
    @acting_user = params[:acting_user]
    @skip_revision = params[:skip_revision] || false

    %i[post_ids topic new_owner acting_user].each do |arg|
      raise ArgumentError.new(arg) if self.instance_variable_get("@#{arg}").blank?
    end
  end

  def change_owner!
    @post_ids.each do |post_id|
      next unless post = Post.with_deleted.find_by(id: post_id, topic_id: @topic.id)

      if post.is_first_post?
        @topic.user = @new_owner
        @topic.recover! if post.user.nil?
      end

      post.topic = @topic
      post.set_owner(@new_owner, @acting_user, @skip_revision)
      PostActionDestroyer.destroy(@new_owner, post, :like, skip_delete_check: true)

      level = post.is_first_post? ? :watching : :tracking
      TopicUser.change(
        @new_owner.id,
        @topic.id,
        notification_level: NotificationLevels.topic_levels[level],
        posted: true,
      )

      if post ==
           @topic
             .posts
             .order("post_number DESC")
             .where("NOT hidden AND posts.deleted_at IS NULL")
             .first
        @topic.last_poster = @new_owner
      end

      @topic.update_statistics

      @new_owner.user_stat.update(
        first_post_created_at: @new_owner.reload.posts.order("created_at ASC").first&.created_at,
      )

      Post.where(topic_id: @topic.id, reply_to_post_number: post.post_number).update_all(
        reply_to_user_id: @new_owner.id,
      )

      @topic.save!(validate: false)
    end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe PostOwnerChanger do
  describe "#change_owner!" do
    fab!(:editor) { Fabricate(:admin) }
    fab!(:user_a) { Fabricate(:user) }
    let(:p1) { create_post(post_number: 1) }
    let(:topic) { p1.topic }
    let(:p2) { create_post(topic: topic, post_number: 2) }
    let(:p3) { create_post }

    it "raises an error with a parameter missing" do
      expect {
        PostOwnerChanger.new(
          post_ids: [p1.id],
          topic_id: topic.id,
          new_owner: nil,
          acting_user: editor,
        )
      }.to raise_error(ArgumentError, /new_owner/)
    end

    it "calls PostRevisor" do
      PostRevisor.any_instance.expects(:revise!)
      PostOwnerChanger.new(
        post_ids: [p1.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
    end

    it "changes the user" do
      bumped_at = freeze_time topic.bumped_at
      now = Time.zone.now
      freeze_time(now - 1.day)

      old_user = p1.user
      PostActionCreator.like(user_a, p1)
      p1.reload
      expect(p1.topic.like_count).to eq(1)

      freeze_time(now)
      PostOwnerChanger.new(
        post_ids: [p1.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      p1.reload
      expect(p1.topic.like_count).to eq(0)
      expect(p1.topic.bumped_at).to eq_time(bumped_at)
      expect(p1.topic.last_post_user_id).to eq(user_a.id)
      expect(old_user).not_to eq(p1.user)
      expect(p1.user).to eq(user_a)
    end

    it "changes multiple posts" do
      PostOwnerChanger.new(
        post_ids: [p1.id, p2.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      p1.reload
      p2.reload
      expect(p1.user).not_to eq(nil)
      expect(p1.user).to eq(user_a)
      expect(p1.user).to eq(p2.user)
    end

    it "ignores posts in other topics" do
      PostOwnerChanger.new(
        post_ids: [p1.id, p3.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      p1.reload
      p3.reload
      expect(p1.user).to eq(user_a)

      expect(p3.topic_id).not_to eq(p1.topic_id)
      expect(p2.user).not_to eq(user_a)
    end

    it "skips creating new post revision if skip_revision is true" do
      PostOwnerChanger.new(
        post_ids: [p1.id, p2.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
        skip_revision: true,
      ).change_owner!
      p1.reload
      p2.reload
      expect(p1.revisions.size).to eq(0)
      expect(p2.revisions.size).to eq(0)
    end

    it "changes the user even when the post does not pass validation" do
      p1.update_attribute(:raw, "foo")
      PostOwnerChanger.new(
        post_ids: [p1.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      expect(p1.reload.user).to eq(user_a)
    end

    it "changes the user even when the topic does not pass validation" do
      topic.update_column(:title, "short")

      PostOwnerChanger.new(
        post_ids: [p1.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      expect(p1.reload.user).to eq(user_a)
    end

    it "changes the owner when the post is deleted" do
      p4 = Fabricate(:post, topic: topic, reply_to_post_number: p2.post_number)
      PostDestroyer.new(editor, p4).destroy

      PostOwnerChanger.new(
        post_ids: [p4.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      expect(p4.reload.user).to eq(user_a)
    end

    it "sets 'posted' for TopicUser to true" do
      PostOwnerChanger.new(
        post_ids: [p1.id],
        topic_id: topic.id,
        new_owner: user_a,
        acting_user: editor,
      ).change_owner!
      expect(TopicUser.find_by(topic_id: topic.id, user_id: user_a.id).posted).to eq(true)
    end

    context "when setting topic notification level for the new owner" do
      let(:p4) { create_post(post_number: 2, topic: topic) }

      it "'watching' if the first post gets a new owner" do
        PostOwnerChanger.new(
          post_ids: [p1.id],
          topic_id: topic.id,
          new_owner: user_a,
          acting_user: editor,
        ).change_owner!
        tu = TopicUser.find_by(user_id: user_a.id, topic_id: topic.id)
        expect(tu.notification_level).to eq(3)
      end

      it "'tracking' if other than the first post gets a new owner" do
        PostOwnerChanger.new(
          post_ids: [p4.id],
          topic_id: topic.id,
          new_owner: user_a,
          acting_user: editor,
        ).change_owner!
        tu = TopicUser.find_by(user_id: user_a.id, topic_id: topic.id)
        expect(tu.notification_level).to eq(2)
      end
    end

    context "with integration tests" do
      subject(:change_owners) do
        PostOwnerChanger.new(
          post_ids: [p1.id, p2.id],
          topic_id: topic.id,
          new_owner: user_a,
          acting_user: editor,
        ).change_owner!
      end

      let(:p1user) { p1.user }
      let(:p2user) { p2.user }

      before do
        topic.update!(user_id: p1user.id)

        p1user.user_stat.update!(
          topic_count: 1,
          post_count: 0,
          first_post_created_at: p1.created_at,
        )

        p2user.user_stat.update!(
          topic_count: 0,
          post_count: 1,
          first_post_created_at: p2.created_at,
        )

        UserAction.create!(
          action_type: UserAction::NEW_TOPIC,
          user_id: p1user.id,
          acting_user_id: p1user.id,
          target_post_id: -1,
          target_topic_id: p1.topic_id,
          created_at: p1.created_at,
        )
        UserAction.create!(
          action_type: UserAction::REPLY,
          user_id: p2user.id,
          acting_user_id: p2user.id,
          target_post_id: p2.id,
          target_topic_id: p2.topic_id,
          created_at: p2.created_at,
        )

        UserActionManager.enable
      end

      it "updates users' topic and post counts" do
        PostActionCreator.like(p2user, p1)
        expect(p1user.user_stat.reload.likes_received).to eq(1)

        change_owners

        p1user.reload
        p2user.reload
        user_a.reload
        expect(p1user.topic_count).to eq(0)
        expect(p1user.post_count).to eq(0)
        expect(p2user.topic_count).to eq(0)
        expect(p2user.post_count).to eq(0)
        expect(user_a.topic_count).to eq(1)
        expect(user_a.post_count).to eq(1)

        p1_user_stat = p1user.user_stat

        expect(p1_user_stat.first_post_created_at).to eq(nil)
        expect(p1_user_stat.likes_received).to eq(0)

        p2_user_stat = p2user.user_stat

        expect(p2_user_stat.first_post_created_at).to eq(nil)

        user_a_stat = user_a.user_stat

        expect(user_a_stat.first_post_created_at).to be_present
        expect(user_a_stat.likes_received).to eq(1)
      end

      it "handles whispers" do
        whisper =
          PostCreator.new(
            editor,
            topic_id: p1.topic_id,
            reply_to_post_number: 1,
            post_type: Post.types[:whisper],
            raw: "this is a whispered reply",
          ).create

        user_stat = editor.user_stat

        expect {
          PostOwnerChanger.new(
            post_ids: [whisper.id],
            topic_id: topic.id,
            new_owner: Fabricate(:admin),
            acting_user: editor,
          ).change_owner!
        }.to_not change { user_stat.reload.post_count }
      end

      context "with private message topic" do
        let(:pm) { create_post(archetype: "private_message", target_usernames: [p2user.username]) }
        let(:pm_poster) { pm.user }

        it "should update users' counts" do
          PostActionCreator.like(p2user, pm)

          expect {
            PostOwnerChanger.new(
              post_ids: [pm.id],
              topic_id: pm.topic_id,
              new_owner: user_a,
              acting_user: editor,
            ).change_owner!
          }.to_not change { pm_poster.user_stat.post_count }

          expect(pm_poster.user_stat.likes_received).to eq(0)

          user_a_stat = user_a.user_stat
          expect(user_a_stat.first_post_created_at).to be_present
          expect(user_a_stat.likes_received).to eq(0)
          expect(user_a_stat.post_count).to eq(0)
        end
      end

      it "updates UserAction records" do
        g = Guardian.new(editor)
        expect(UserAction.stats(user_a.id, g)).to eq([])

        change_owners

        expect(UserAction.stats(p1user.id, g)).to eq([])
        expect(UserAction.stats(p2user.id, g)).to eq([])
        stats = UserAction.stats(user_a.id, g)
        expect(stats.size).to eq(2)
        expect(stats[0].action_type).to eq(UserAction::NEW_TOPIC)
        expect(stats[0].count).to eq(1)
        expect(stats[1].action_type).to eq(UserAction::REPLY)
        expect(stats[1].count).to eq(1)
      end

      it "updates reply_to_user_id" do
        p4 =
          Fabricate(
            :post,
            topic: topic,
            reply_to_post_number: p1.post_number,
            reply_to_user_id: p1.user_id,
          )
        p5 =
          Fabricate(
            :post,
            topic: topic,
            reply_to_post_number: p2.post_number,
            reply_to_user_id: p2.user_id,
          )

        PostOwnerChanger.new(
          post_ids: [p1.id],
          topic_id: topic.id,
          new_owner: user_a,
          acting_user: editor,
        ).change_owner!
        p4.reload
        p5.reload

        expect(p4.reply_to_user_id).to eq(user_a.id)
        expect(p5.reply_to_user_id).to eq(p2.user_id)
      end
    end
  end
end


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

class TopicTimestampChanger
  class InvalidTimestampError < StandardError
  end

  def initialize(timestamp:, topic: nil, topic_id: nil)
    @topic = topic || Topic.with_deleted.find(topic_id)
    @posts = @topic.posts
    @current_timestamp = Time.zone.now
    @timestamp = Time.zone.at(timestamp)

    raise InvalidTimestampError if @timestamp.to_f > @current_timestamp.to_f

    @time_difference = calculate_time_difference
  end

  def change!
    ActiveRecord::Base.transaction do
      last_posted_at = @timestamp

      @posts.each do |post|
        if post.is_first_post?
          update_post(post, @timestamp)
        else
          new_created_at = Time.at(post.created_at.to_f + @time_difference)
          new_created_at = @current_timestamp if new_created_at > @current_timestamp
          last_posted_at = new_created_at if new_created_at > last_posted_at
          update_post(post, new_created_at)
        end
      end

      @topic.reset_bumped_at
      update_topic(last_posted_at)

      yield(@topic) if block_given?
    end

    # Burst the cache for stats
    [AdminDashboardData, About].each { |klass| Discourse.redis.del klass.stats_cache_key }
  end

  private

  def calculate_time_difference
    @timestamp - @topic.created_at
  end

  def update_topic(last_posted_at)
    @topic.update(created_at: @timestamp, updated_at: @timestamp, last_posted_at: last_posted_at)
  end

  def update_post(post, timestamp)
    post.update(created_at: timestamp, updated_at: timestamp)
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe TopicTimestampChanger do
  describe "#change!" do
    let(:old_timestamp) { Time.zone.now }
    let(:topic) { Fabricate(:topic, created_at: old_timestamp) }
    let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) }
    let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) }

    context "when new timestamp is in the future" do
      let(:new_timestamp) { old_timestamp + 2.day }

      it "should raise the right error" do
        expect {
          TopicTimestampChanger.new(topic: topic, timestamp: new_timestamp.to_f).change!
        }.to raise_error(TopicTimestampChanger::InvalidTimestampError)
      end
    end

    context "when new timestamp is in the past" do
      let(:new_timestamp) { old_timestamp - 2.day }

      it "changes the timestamp of the topic and opening post" do
        freeze_time
        TopicTimestampChanger.new(topic: topic, timestamp: new_timestamp.to_f).change!

        topic.reload
        p1.reload
        p2.reload
        last_post_created_at = p2.created_at

        expect(topic.created_at).to eq_time(new_timestamp)
        expect(topic.updated_at).to eq_time(new_timestamp)
        expect(topic.bumped_at).to eq_time(last_post_created_at)
        expect(topic.last_posted_at).to eq_time(last_post_created_at)

        expect(p1.created_at).to eq_time(new_timestamp)
        expect(p1.updated_at).to eq_time(new_timestamp)

        expect(p2.created_at).to eq_time(new_timestamp + 1.day)
        expect(p2.updated_at).to eq_time(new_timestamp + 1.day)
      end

      context "when posts have timestamps in the future" do
        it "should set the new timestamp as the default timestamp" do
          new_timestamp = freeze_time

          p3 = Fabricate(:post, topic: topic, created_at: new_timestamp + 3.days)
          TopicTimestampChanger.new(topic: topic, timestamp: new_timestamp.to_f).change!

          p3.reload

          expect(p3.created_at).to eq_time(new_timestamp)
          expect(p3.updated_at).to eq_time(new_timestamp)
        end
      end
    end

    it "deletes the stats cache" do
      Discourse.redis.set AdminDashboardData.stats_cache_key, "X"
      Discourse.redis.set About.stats_cache_key, "X"

      TopicTimestampChanger.new(topic: topic, timestamp: Time.zone.now.to_f).change!

      expect(Discourse.redis.get(AdminDashboardData.stats_cache_key)).to eq(nil)
      expect(Discourse.redis.get(About.stats_cache_key)).to eq(nil)
    end
  end
end


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

class TrustLevelGranter
  def initialize(trust_level, user)
    @trust_level, @user = trust_level, user
  end

  def self.grant(trust_level, user)
    TrustLevelGranter.new(trust_level, user).grant
  end

  def grant
    if @user.trust_level < @trust_level
      @user.change_trust_level!(@trust_level)
      @user.save!
    end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe TrustLevelGranter do
  describe "grant" do
    it "grants trust level" do
      user = Fabricate(:user, email: "foo@bar.com", trust_level: 0)
      TrustLevelGranter.grant(3, user)

      user.reload
      expect(user.trust_level).to eq(3)
    end
  end
end


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

class ThemeSettingsMigrationsRunner
  Migration = Struct.new(:version, :name, :original_name, :code, :theme_field_id)

  MIGRATION_ENTRY_POINT_JS = <<~JS
    const migrate = require("discourse/theme/migration")?.default;
    function main(settingsObj) {
      if (!migrate) {
        throw new Error("no_exported_migration_function");
      }
      if (typeof migrate !== "function") {
        throw new Error("default_export_is_not_a_function");
      }
      const map = new Map(Object.entries(settingsObj));
      const updatedMap = migrate(map);
      if (!updatedMap) {
        throw new Error("migration_function_no_returned_value");
      }
      if (!(updatedMap instanceof Map)) {
        throw new Error("migration_function_wrong_return_type");
      }
      return Object.fromEntries(updatedMap.entries());
    }
  JS

  private_constant :Migration, :MIGRATION_ENTRY_POINT_JS

  def self.loader_js_lib_content
    @loader_js_lib_content ||=
      File.read(
        File.join(
          Rails.root,
          "app/assets/javascripts/node_modules/loader.js/dist/loader/loader.js",
        ),
      )
  end

  def initialize(theme, limit: 100, timeout: 100, memory: 2.megabytes)
    @theme = theme
    @limit = limit
    @timeout = timeout
    @memory = memory
  end

  def run
    fields = lookup_pending_migrations_fields

    count = fields.count
    return [] if count == 0

    raise_error("themes.import_error.migrations.too_many_pending_migrations") if count > @limit

    migrations = convert_fields_to_migrations(fields)
    migrations.sort_by!(&:version)

    current_migration_version =
      @theme.theme_settings_migrations.order(version: :desc).pick(:version)
    current_migration_version ||= -Float::INFINITY

    current_settings = lookup_overriden_settings

    migrations.map do |migration|
      if migration.version <= current_migration_version
        raise_error(
          "themes.import_error.migrations.out_of_sequence",
          name: migration.original_name,
          current: current_migration_version,
        )
      end

      migrated_settings = execute(migration, current_settings)
      results = {
        version: migration.version,
        name: migration.name,
        original_name: migration.original_name,
        theme_field_id: migration.theme_field_id,
        settings_before: current_settings,
        settings_after: migrated_settings,
      }
      current_settings = migrated_settings
      current_migration_version = migration.version
      results
    rescue DiscourseJsProcessor::TranspileError => error
      raise_error(
        "themes.import_error.migrations.syntax_error",
        name: migration.original_name,
        error: error.message,
      )
    rescue MiniRacer::V8OutOfMemoryError
      raise_error(
        "themes.import_error.migrations.exceeded_memory_limit",
        name: migration.original_name,
      )
    rescue MiniRacer::ScriptTerminatedError
      raise_error("themes.import_error.migrations.timed_out", name: migration.original_name)
    rescue MiniRacer::RuntimeError => error
      message = error.message
      if message.include?("no_exported_migration_function")
        raise_error(
          "themes.import_error.migrations.no_exported_function",
          name: migration.original_name,
        )
      elsif message.include?("default_export_is_not_a_function")
        raise_error(
          "themes.import_error.migrations.default_export_not_a_function",
          name: migration.original_name,
        )
      elsif message.include?("migration_function_no_returned_value")
        raise_error(
          "themes.import_error.migrations.no_returned_value",
          name: migration.original_name,
        )
      elsif message.include?("migration_function_wrong_return_type")
        raise_error(
          "themes.import_error.migrations.wrong_return_type",
          name: migration.original_name,
        )
      else
        raise_error(
          "themes.import_error.migrations.runtime_error",
          name: migration.original_name,
          error: message,
        )
      end
    end
  end

  private

  def lookup_pending_migrations_fields
    @theme
      .migration_fields
      .left_joins(:theme_settings_migration)
      .where(theme_settings_migration: { id: nil })
  end

  def convert_fields_to_migrations(fields)
    fields.map do |field|
      match_data = /\A(?<version>\d{4})-(?<name>.+)/.match(field.name)

      if !match_data
        raise_error("themes.import_error.migrations.invalid_filename", filename: field.name)
      end

      version = match_data[:version].to_i
      name = match_data[:name]
      original_name = field.name

      Migration.new(
        version: version,
        name: name,
        original_name: original_name,
        code: field.value,
        theme_field_id: field.id,
      )
    end
  end

  def lookup_overriden_settings
    hash = {}
    @theme.theme_settings.each { |row| hash[row.name] = ThemeSettingsManager.cast_row_value(row) }
    hash
  end

  def execute(migration, settings)
    context = MiniRacer::Context.new(timeout: @timeout, max_memory: @memory)

    context.eval(self.class.loader_js_lib_content, filename: "loader.js")

    context.eval(
      DiscourseJsProcessor.transpile(migration.code, "", "discourse/theme/migration"),
      filename: "theme-#{@theme.id}-migration.js",
    )

    context.eval(MIGRATION_ENTRY_POINT_JS, filename: "migration-entrypoint.js")
    context.call("main", settings)
  ensure
    context&.dispose
  end

  def raise_error(message_key, **i18n_args)
    raise Theme::SettingsMigrationError.new(I18n.t(message_key, **i18n_args))
  end
end

```

Response:
# frozen_string_literal: true

describe ThemeSettingsMigrationsRunner do
  fab!(:theme)
  fab!(:migration_field) { Fabricate(:migration_theme_field, version: 1, theme: theme) }
  fab!(:settings_field) { Fabricate(:settings_theme_field, theme: theme, value: <<~YAML) }
      integer_setting: 1
      boolean_setting: true
      string_setting: ""
    YAML

  describe "#run" do
    it "passes values of overridden settings only to migrations" do
      theme.update_setting(:integer_setting, 1)
      theme.update_setting(:string_setting, "osama")
      theme.save!

      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          if (settings.get("integer_setting") !== 1) {
            throw new Error(`expected integer_setting to equal 1, but it's actually ${settings.get("integer_setting")}`);
          }
          if (settings.get("string_setting") !== "osama") {
            throw new Error(`expected string_setting to equal "osama", but it's actually "${settings.get("string_setting")}"`);
          }
          if (settings.size !== 2) {
            throw new Error(`expected the settings map to have only 2 keys, but instead got ${settings.size} keys`);
          }
          return settings;
        }
      JS
      results = described_class.new(theme).run
      expect(results.first[:theme_field_id]).to eq(migration_field.id)
      expect(results.first[:settings_before]).to eq(
        { "integer_setting" => 1, "string_setting" => "osama" },
      )
    end

    it "passes the output of the previous migration as input to the next one" do
      theme.update_setting(:integer_setting, 1)

      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          settings.set("integer_setting", 111);
          return settings;
        }
      JS

      another_migration_field =
        Fabricate(:migration_theme_field, theme: theme, version: 2, value: <<~JS)
        export default function migrate(settings) {
          if (settings.get("integer_setting") !== 111) {
            throw new Error(`expected integer_setting to equal 111, but it's actually ${settings.get("integer_setting")}`);
          }
          settings.set("integer_setting", 222);
          return settings;
        }
      JS

      results = described_class.new(theme).run

      expect(results.size).to eq(2)

      expect(results[0][:theme_field_id]).to eq(migration_field.id)
      expect(results[1][:theme_field_id]).to eq(another_migration_field.id)

      expect(results[0][:settings_before]).to eq({})
      expect(results[0][:settings_after]).to eq({ "integer_setting" => 111 })

      expect(results[1][:settings_before]).to eq({ "integer_setting" => 111 })
      expect(results[1][:settings_after]).to eq({ "integer_setting" => 222 })
    end

    it "doesn't run migrations that have already been ran" do
      Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field)

      pending_field = Fabricate(:migration_theme_field, theme: theme, version: 23)

      results = described_class.new(theme).run

      expect(results.size).to eq(1)

      expect(results.first[:version]).to eq(23)
      expect(results.first[:theme_field_id]).to eq(pending_field.id)
    end

    it "doesn't error when no migrations have been ran yet" do
      results = described_class.new(theme).run

      expect(results.size).to eq(1)
      expect(results.first[:version]).to eq(1)
      expect(results.first[:theme_field_id]).to eq(migration_field.id)
    end

    it "doesn't error when there are no pending migrations" do
      Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field)

      results = described_class.new(theme).run

      expect(results.size).to eq(0)
    end

    it "raises an error when there are too many pending migrations" do
      Fabricate(:migration_theme_field, theme: theme, version: 2)

      expect do described_class.new(theme, limit: 1).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.too_many_pending_migrations"),
      )
    end

    it "raises an error if a migration field has a badly formatted name" do
      migration_field.update_attribute(:name, "020-some-name")

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.invalid_filename", filename: "020-some-name"),
      )

      migration_field.update_attribute(:name, "0020some-name")

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.invalid_filename", filename: "0020some-name"),
      )

      migration_field.update_attribute(:name, "0020")

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.invalid_filename", filename: "0020"),
      )
    end

    it "raises an error if a pending migration has version lower than the last ran migration" do
      migration_field.update!(name: "0020-some-name")
      Fabricate(:theme_settings_migration, theme: theme, theme_field: migration_field, version: 20)

      Fabricate(:migration_theme_field, theme: theme, version: 19, name: "0019-failing-migration")

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t(
          "themes.import_error.migrations.out_of_sequence",
          name: "0019-failing-migration",
          current: 20,
        ),
      )
    end

    it "detects bad syntax in migrations and raises an error" do
      migration_field.update!(value: <<~JS)
        export default function migrate() {
      JS
      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t(
          "themes.import_error.migrations.syntax_error",
          name: "0001-some-name",
          error:
            'SyntaxError: "/discourse/theme/migration: Unexpected token (2:0)\n\n  1 | export default function migrate() {\n> 2 |\n    | ^"',
        ),
      )
    end

    it "imposes memory limit on migrations and raises an error if they exceed the limit" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          let a = new Array(10000);
          while(true) {
            a = a.concat(new Array(10000));
          }
          return settings;
        }
      JS

      expect do described_class.new(theme, memory: 10.kilobytes).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.exceeded_memory_limit", name: "0001-some-name"),
      )
    end

    it "imposes time limit on migrations and raises an error if they exceed the limit" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          let a = 1;
          while(true) {
            a += 1;
          }
          return settings;
        }
      JS

      expect do described_class.new(theme, timeout: 10).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.timed_out", name: "0001-some-name"),
      )
    end

    it "raises a clear error message when the migration file doesn't export anything" do
      migration_field.update!(value: <<~JS)
        function migrate(settings) {
          return settings;
        }
      JS

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.no_exported_function", name: "0001-some-name"),
      )
    end

    it "raises a clear error message when the migration file exports the default as something that's not a function" do
      migration_field.update!(value: <<~JS)
        export function migrate(settings) {
          return settings;
        }

        const AA = 1;
        export default AA;
      JS

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t(
          "themes.import_error.migrations.default_export_not_a_function",
          name: "0001-some-name",
        ),
      )
    end

    it "raises a clear error message when the migration function doesn't return anything" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {}
      JS

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.no_returned_value", name: "0001-some-name"),
      )
    end

    it "raises a clear error message when the migration function doesn't return a Map" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          return {};
        }
      JS

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t("themes.import_error.migrations.wrong_return_type", name: "0001-some-name"),
      )
    end

    it "surfaces runtime errors that occur within the migration" do
      migration_field.update!(value: <<~JS)
        export default function migrate(settings) {
          null.toString();
          return settings;
        }
      JS

      expect do described_class.new(theme).run end.to raise_error(
        Theme::SettingsMigrationError,
        I18n.t(
          "themes.import_error.migrations.runtime_error",
          name: "0001-some-name",
          error: "TypeError: Cannot read properties of null (reading 'toString')",
        ),
      )
    end

    it "returns a list of objects that each has data representing the migration and the results" do
      results = described_class.new(theme).run

      expect(results[0][:version]).to eq(1)
      expect(results[0][:name]).to eq("some-name")
      expect(results[0][:original_name]).to eq("0001-some-name")
      expect(results[0][:theme_field_id]).to eq(migration_field.id)
      expect(results[0][:settings_before]).to eq({})
      expect(results[0][:settings_after]).to eq({})
    end
  end
end


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

# Responsible for destroying a User record
class UserDestroyer
  class PostsExistError < RuntimeError
  end

  def initialize(actor)
    @actor = actor
    raise Discourse::InvalidParameters.new("acting user is nil") unless @actor && @actor.is_a?(User)
    @guardian = Guardian.new(actor)
  end

  # Returns false if the user failed to be deleted.
  # Returns a frozen instance of the User if the delete succeeded.
  def destroy(user, opts = {})
    raise Discourse::InvalidParameters.new("user is nil") unless user && user.is_a?(User)
    raise PostsExistError if !opts[:delete_posts] && user.posts.joins(:topic).count != 0
    @guardian.ensure_can_delete_user!(user)

    # default to using a transaction
    opts[:transaction] = true if opts[:transaction] != false

    prepare_for_destroy(user) if opts[:prepare_for_destroy] == true

    result = nil

    optional_transaction(open_transaction: opts[:transaction]) do
      UserSecurityKey.where(user_id: user.id).delete_all
      Bookmark.where(user_id: user.id).delete_all
      Draft.where(user_id: user.id).delete_all
      Reviewable.where(created_by_id: user.id).delete_all

      category_topic_ids = Category.where("topic_id IS NOT NULL").pluck(:topic_id)

      if opts[:delete_posts]
        DiscoursePluginRegistry.user_destroyer_on_content_deletion_callbacks.each do |cb|
          cb.call(user, @guardian, opts)
        end

        agree_with_flags(user) if opts[:delete_as_spammer]
        block_external_urls(user) if opts[:block_urls]
        delete_posts(user, category_topic_ids, opts)
      end

      user.post_actions.find_each { |post_action| post_action.remove_act!(Discourse.system_user) }

      # Add info about the user to staff action logs
      UserHistory.staff_action_records(
        Discourse.system_user,
        acting_user: user.username,
      ).update_all(
        ["details = CONCAT(details, ?)", "\nuser_id: #{user.id}\nusername: #{user.username}"],
      )

      # keep track of emails used
      user_emails = user.user_emails.pluck(:email)

      if result = user.destroy
        if opts[:block_email]
          user_emails.each do |email|
            ScreenedEmail.block(email, ip_address: result.ip_address)&.record_match!
          end
        end

        if opts[:block_ip] && result.ip_address
          ScreenedIpAddress.watch(result.ip_address)&.record_match!
          if result.registration_ip_address && result.ip_address != result.registration_ip_address
            ScreenedIpAddress.watch(result.registration_ip_address)&.record_match!
          end
        end

        Post.unscoped.where(user_id: result.id).update_all(user_id: nil)

        # If this user created categories, fix those up:
        Category
          .where(user_id: result.id)
          .each do |c|
            c.user_id = Discourse::SYSTEM_USER_ID
            c.save!
            if topic = Topic.unscoped.find_by(id: c.topic_id)
              topic.recover!
              topic.user_id = Discourse::SYSTEM_USER_ID
              topic.save!
            end
          end

        Invite
          .where(email: user_emails)
          .each do |invite|
            # invited_users will be removed by dependent destroy association when user is destroyed
            invite.invited_groups.destroy_all
            invite.topic_invites.destroy_all
            invite.destroy
          end

        unless opts[:quiet]
          if @actor == user
            deleted_by = Discourse.system_user
            opts[:context] = I18n.t("staff_action_logs.user_delete_self", url: opts[:context])
          else
            deleted_by = @actor
          end
          StaffActionLogger.new(deleted_by).log_user_deletion(user, opts.slice(:context))
          if opts.slice(:context).blank?
            Rails.logger.warn("User destroyed without context from: #{caller_locations(14, 1)[0]}")
          end
        end
        MessageBus.publish "/logout/#{result.id}", result.id, user_ids: [result.id]
      end
    end

    # After the user is deleted, remove the reviewable
    if reviewable = ReviewableUser.pending.find_by(target: user)
      reviewable.perform(@actor, :delete_user)
    end

    result
  end

  protected

  def block_external_urls(user)
    TopicLink
      .where(user: user, internal: false)
      .find_each do |link|
        next if Oneboxer.engine(link.url) != Onebox::Engine::AllowlistedGenericOnebox
        ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match!
      end
  end

  def agree_with_flags(user)
    ReviewableFlaggedPost
      .where(target_created_by: user)
      .find_each do |reviewable|
        if reviewable.actions_for(@guardian).has?(:agree_and_keep)
          reviewable.perform(@actor, :agree_and_keep)
        end
      end

    ReviewablePost
      .where(target_created_by: user)
      .find_each do |reviewable|
        if reviewable.actions_for(@guardian).has?(:reject_and_delete)
          reviewable.perform(@actor, :reject_and_delete)
        end
      end
  end

  def delete_posts(user, category_topic_ids, opts)
    user.posts.find_each do |post|
      if post.is_first_post? && category_topic_ids.include?(post.topic_id)
        post.update!(user: Discourse.system_user)
      else
        PostDestroyer.new(@actor.staff? ? @actor : Discourse.system_user, post).destroy
      end

      if post.topic && post.is_first_post?
        Topic.unscoped.where(id: post.topic_id).update_all(user_id: nil)
      end
    end
  end

  def prepare_for_destroy(user)
    PostAction.where(user_id: user.id).delete_all
    UserAction.where(
      "user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id",
      user_id: user.id,
    ).delete_all
    PostTiming.where(user_id: user.id).delete_all
    TopicViewItem.where(user_id: user.id).delete_all
    TopicUser.where(user_id: user.id).delete_all
    TopicAllowedUser.where(user_id: user.id).delete_all
    Notification.where(user_id: user.id).delete_all
  end

  def optional_transaction(open_transaction: true)
    if open_transaction
      User.transaction { yield }
    else
      yield
    end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe UserDestroyer do
  fab!(:user) { Fabricate(:user_with_secondary_email) }
  fab!(:admin)

  describe ".new" do
    it "raises an error when user is nil" do
      expect { UserDestroyer.new(nil) }.to raise_error(Discourse::InvalidParameters)
    end

    it "raises an error when user is not a User" do
      expect { UserDestroyer.new(5) }.to raise_error(Discourse::InvalidParameters)
    end
  end

  describe "#destroy" do
    it "raises an error when user is nil" do
      expect { UserDestroyer.new(admin).destroy(nil) }.to raise_error(Discourse::InvalidParameters)
    end

    it "raises an error when user is not a User" do
      expect { UserDestroyer.new(admin).destroy("nothing") }.to raise_error(
        Discourse::InvalidParameters,
      )
    end

    it "raises an error when regular user tries to delete another user" do
      expect { UserDestroyer.new(user).destroy(Fabricate(:user)) }.to raise_error(
        Discourse::InvalidAccess,
      )
    end

    shared_examples "successfully destroy a user" do
      it "should delete the user" do
        expect { destroy }.to change { User.count }.by(-1)
      end

      it "should return the deleted user record" do
        return_value = destroy
        expect(return_value).to eq(user)
        expect(return_value).to be_destroyed
      end

      it "should log the action" do
        StaffActionLogger.any_instance.expects(:log_user_deletion).with(user, anything).once
        destroy
      end

      it "should not log the action if quiet is true" do
        expect {
          UserDestroyer.new(admin).destroy(user, destroy_opts.merge(quiet: true))
        }.to_not change { UserHistory.where(action: UserHistory.actions[:delete_user]).count }
      end

      it "triggers a extensibility event" do
        event = DiscourseEvent.track_events { destroy }.last

        expect(event[:event_name]).to eq(:user_destroyed)
        expect(event[:params].first).to eq(user)
      end
    end

    shared_examples "email block list" do
      it "doesn't add email to block list by default" do
        ScreenedEmail.expects(:block).never
        destroy
      end

      it "adds emails to block list if block_email is true" do
        expect {
          UserDestroyer.new(admin).destroy(user, destroy_opts.merge(block_email: true))
        }.to change { ScreenedEmail.count }.by(2)
      end
    end

    context "when user deletes self" do
      subject(:destroy) { UserDestroyer.new(user).destroy(user, destroy_opts) }

      let(:destroy_opts) { { delete_posts: true, context: "/u/username/preferences/account" } }

      include_examples "successfully destroy a user"

      it "should log proper context" do
        destroy
        expect(UserHistory.where(action: UserHistory.actions[:delete_user]).last.context).to eq(
          I18n.t("staff_action_logs.user_delete_self", url: "/u/username/preferences/account"),
        )
      end
    end

    context "when context is missing" do
      it "logs warning message if context is missing" do
        logger = track_log_messages { UserDestroyer.new(admin).destroy(user) }
        expect(logger.warnings).to include(/User destroyed without context from:/)
      end
    end

    context "with a reviewable post" do
      let!(:reviewable) { Fabricate(:reviewable, created_by: user) }

      it "removes the queued post" do
        UserDestroyer.new(admin).destroy(user)
        expect(Reviewable.where(created_by_id: user.id).count).to eq(0)
      end
    end

    context "with a reviewable user" do
      let(:reviewable) { Fabricate(:reviewable, created_by: admin) }

      it "sets the reviewable user as rejected" do
        UserDestroyer.new(admin).destroy(reviewable.target)

        expect(reviewable.reload).to be_rejected
      end
    end

    context "with a directory item record" do
      it "removes the directory item" do
        DirectoryItem.create!(
          user: user,
          period_type: 1,
          likes_received: 0,
          likes_given: 0,
          topics_entered: 0,
          topic_count: 0,
          post_count: 0,
        )
        UserDestroyer.new(admin).destroy(user)
        expect(DirectoryItem.where(user_id: user.id).count).to eq(0)
      end
    end

    context "with a draft" do
      let!(:draft) { Draft.set(user, "test", 0, "test") }

      it "removed the draft" do
        UserDestroyer.new(admin).destroy(user)
        expect(Draft.where(user_id: user.id).count).to eq(0)
      end
    end

    context "when user has posts" do
      let!(:topic_starter) { Fabricate(:user) }
      let!(:topic) { Fabricate(:topic, user: topic_starter) }
      let!(:first_post) { Fabricate(:post, user: topic_starter, topic: topic) }
      let!(:post) { Fabricate(:post, user: user, topic: topic) }

      context "when delete_posts is false" do
        subject(:destroy) { UserDestroyer.new(admin).destroy(user) }

        before do
          user.stubs(:post_count).returns(1)
          user.stubs(:first_post_created_at).returns(Time.zone.now)
        end

        it "should raise the right error" do
          StaffActionLogger.any_instance.expects(:log_user_deletion).never
          expect { destroy }.to raise_error(UserDestroyer::PostsExistError)
          expect(user.reload.id).to be_present
        end
      end

      context "when delete_posts is true" do
        let(:destroy_opts) { { delete_posts: true } }

        context "when staff deletes user" do
          subject(:destroy) { UserDestroyer.new(admin).destroy(user, destroy_opts) }

          include_examples "successfully destroy a user"
          include_examples "email block list"

          it "deletes the posts" do
            destroy
            expect(post.reload.deleted_at).not_to eq(nil)
            expect(post.user_id).to eq(nil)
          end

          it "does not delete topics started by others in which the user has replies" do
            destroy
            expect(topic.reload.deleted_at).to eq(nil)
            expect(topic.user_id).not_to eq(nil)
          end

          it "deletes topics started by the deleted user" do
            spammer_topic = Fabricate(:topic, user: user)
            Fabricate(:post, user: user, topic: spammer_topic)
            destroy
            expect(spammer_topic.reload.deleted_at).not_to eq(nil)
            expect(spammer_topic.user_id).to eq(nil)
          end

          context "when delete_as_spammer is true" do
            before { destroy_opts[:delete_as_spammer] = true }

            it "approves reviewable flags" do
              spammer_post = Fabricate(:post, user: user)
              reviewable = PostActionCreator.inappropriate(admin, spammer_post).reviewable
              expect(reviewable).to be_pending

              destroy

              reviewable.reload
              expect(reviewable).to be_approved
            end

            it "rejects pending posts" do
              post = Fabricate(:post, user: user)
              reviewable =
                Fabricate(
                  :reviewable,
                  type: "ReviewablePost",
                  target_type: "Post",
                  target_id: post.id,
                  created_by: Discourse.system_user,
                  target_created_by: user,
                )

              expect(reviewable).to be_pending

              destroy

              reviewable.reload
              expect(reviewable).to be_rejected
            end
          end
        end

        context "when users deletes self" do
          subject(:destroy) { UserDestroyer.new(user).destroy(user, destroy_opts) }

          include_examples "successfully destroy a user"
          include_examples "email block list"

          it "deletes the posts" do
            destroy
            expect(post.reload.deleted_at).not_to eq(nil)
            expect(post.user_id).to eq(nil)
          end
        end
      end
    end

    context "when user was invited" do
      it "should delete the invite of user" do
        invite = Fabricate(:invite)
        topic_invite = invite.topic_invites.create!(topic: Fabricate(:topic))
        invited_group = invite.invited_groups.create!(group: Fabricate(:group))
        user = Fabricate(:user)
        user.user_emails.create!(email: invite.email)

        UserDestroyer.new(admin).destroy(user)

        expect(Invite.exists?(invite.id)).to eq(false)
        expect(InvitedGroup.exists?(invited_group.id)).to eq(false)
        expect(TopicInvite.exists?(topic_invite.id)).to eq(false)
      end
    end

    context "when user created category" do
      let!(:topic) { Fabricate(:topic, user: user) }
      let!(:first_post) { Fabricate(:post, user: user, topic: topic) }
      let!(:second_post) { Fabricate(:post, user: user, topic: topic) }
      let!(:category) { Fabricate(:category, user: user, topic_id: topic.id) }

      it "changes author of first category post to system user and still deletes second post" do
        UserDestroyer.new(admin).destroy(user, delete_posts: true)

        expect(first_post.reload.deleted_at).to eq(nil)
        expect(first_post.user_id).to eq(Discourse.system_user.id)

        expect(second_post.reload.deleted_at).not_to eq(nil)
        expect(second_post.user_id).to eq(nil)
      end
    end

    context "when user has no posts, but user_stats table has post_count > 0" do
      subject(:destroy) { UserDestroyer.new(user).destroy(user, delete_posts: false) }

      let(:destroy_opts) { {} }

      before do
        # out of sync user_stat data shouldn't break UserDestroyer
        user.user_stat.update_attribute(:post_count, 1)
      end

      include_examples "successfully destroy a user"
    end

    context "when user has deleted posts" do
      let!(:deleted_post) { Fabricate(:post, user: user, deleted_at: 1.hour.ago) }

      it "should mark the user's deleted posts as belonging to a nuked user" do
        expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
        expect(deleted_post.reload.user_id).to eq(nil)
      end
    end

    context "when user has no posts" do
      context "when destroy succeeds" do
        subject(:destroy) { UserDestroyer.new(admin).destroy(user) }

        let(:destroy_opts) { {} }

        include_examples "successfully destroy a user"
        include_examples "email block list"
      end

      context "when destroy fails" do
        subject(:destroy) { UserDestroyer.new(admin).destroy(user) }

        it "should not log the action" do
          user.stubs(:destroy).returns(false)
          StaffActionLogger.any_instance.expects(:log_user_deletion).never
          destroy
        end
      end
    end

    context "when user has posts with links" do
      context "with external links" do
        before do
          @post = Fabricate(:post_with_external_links, user: user)
          TopicLink.extract_from(@post)
        end

        it "doesn't add ScreenedUrl records by default" do
          ScreenedUrl.expects(:watch).never
          UserDestroyer.new(admin).destroy(user, delete_posts: true)
        end

        it "adds ScreenedUrl records when :block_urls is true" do
          ScreenedUrl.expects(:watch).with(anything, anything, has_key(:ip_address)).at_least_once
          UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
        end
      end

      context "with internal links" do
        before do
          @post = Fabricate(:post_with_external_links, user: user)
          TopicLink.extract_from(@post)
          TopicLink.where(user: user).update_all(internal: true)
        end

        it "doesn't add ScreenedUrl records" do
          ScreenedUrl.expects(:watch).never
          UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
        end
      end

      context "with oneboxed links" do
        before do
          @post = Fabricate(:post_with_youtube, user: user)
          TopicLink.extract_from(@post)
        end

        it "doesn't add ScreenedUrl records" do
          ScreenedUrl.expects(:watch).never
          UserDestroyer.new(admin).destroy(user, delete_posts: true, block_urls: true)
        end
      end
    end

    context "with ip address screening" do
      it "doesn't create screened_ip_address records by default" do
        ScreenedIpAddress.expects(:watch).never
        UserDestroyer.new(admin).destroy(user)
      end

      context "when block_ip is true" do
        it "creates a new screened_ip_address record" do
          ScreenedIpAddress.expects(:watch).with(user.ip_address).returns(stub_everything)
          UserDestroyer.new(admin).destroy(user, block_ip: true)
        end

        it "creates two new screened_ip_address records when registration_ip_address is different than last ip_address" do
          user.registration_ip_address = "12.12.12.12"
          ScreenedIpAddress.expects(:watch).with(user.ip_address).returns(stub_everything)
          ScreenedIpAddress
            .expects(:watch)
            .with(user.registration_ip_address)
            .returns(stub_everything)
          UserDestroyer.new(admin).destroy(user, block_ip: true)
        end
      end
    end

    context "when user created a category" do
      let!(:category) { Fabricate(:category_with_definition, user: user) }

      it "assigns the system user to the categories" do
        UserDestroyer.new(admin).destroy(user, delete_posts: true)
        expect(category.reload.user_id).to eq(Discourse.system_user.id)
        expect(category.topic).to be_present
        expect(category.topic.user_id).to eq(Discourse.system_user.id)
      end
    end

    describe "Destroying a user with security key" do
      let!(:security_key) { Fabricate(:user_security_key_with_random_credential, user: user) }

      it "removes the security key" do
        UserDestroyer.new(admin).destroy(user)
        expect(UserSecurityKey.where(user_id: user.id).count).to eq(0)
      end
    end

    describe "Destroying a user with a bookmark" do
      let!(:bookmark) { Fabricate(:bookmark, user: user) }

      it "removes the bookmark" do
        UserDestroyer.new(admin).destroy(user)
        expect(Bookmark.where(user_id: user.id).count).to eq(0)
      end
    end

    context "when user liked things" do
      before do
        @topic = Fabricate(:topic, user: Fabricate(:user))
        @post = Fabricate(:post, user: @topic.user, topic: @topic)
        PostActionCreator.like(user, @post)
      end

      it "should destroy the like" do
        expect { UserDestroyer.new(admin).destroy(user, delete_posts: true) }.to change {
          PostAction.count
        }.by(-1)
        expect(@post.reload.like_count).to eq(0)
      end
    end

    context "when user belongs to groups that grant trust level" do
      let(:group) { Fabricate(:group, grant_trust_level: 4) }

      before { group.add(user) }

      it "can delete the user" do
        d = UserDestroyer.new(admin)
        expect { d.destroy(user) }.to change { User.count }.by(-1)
      end

      it "can delete the user if they have a manual locked trust level and have no email" do
        user.update(manual_locked_trust_level: 3)

        UserEmail.where(user: user).delete_all
        user.reload
        expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
      end

      it "can delete the user if they were to fall into another trust level and have no email" do
        g2 = Fabricate(:group, grant_trust_level: 1)
        g2.add(user)

        UserEmail.where(user: user).delete_all
        user.reload
        expect { UserDestroyer.new(admin).destroy(user) }.to change { User.count }.by(-1)
      end
    end

    context "when user has staff action logs" do
      before do
        logger = StaffActionLogger.new(user)
        logger.log_site_setting_change(
          "site_description",
          "Our friendly community",
          "My favourite community",
        )
        logger.log_site_setting_change(
          "site_description",
          "Our friendly community",
          "My favourite community",
          details: "existing details",
        )
      end

      it "should keep the staff action log and add the username" do
        username = user.username
        ids =
          UserHistory.staff_action_records(Discourse.system_user, acting_user: username).map(&:id)
        UserDestroyer.new(admin).destroy(user, delete_posts: true)
        details = UserHistory.where(id: ids).map(&:details)
        expect(details).to contain_exactly(
          "\nuser_id: #{user.id}\nusername: #{username}",
          "existing details\nuser_id: #{user.id}\nusername: #{username}",
        )
      end
    end

    context "when user got an email" do
      let!(:email_log) { Fabricate(:email_log, user: user) }

      it "does not delete the email log" do
        expect { UserDestroyer.new(admin).destroy(user, delete_posts: true) }.to_not change {
          EmailLog.count
        }
      end
    end
  end
end


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

module WildcardDomainChecker
  def self.check_domain(domain, external_domain)
    escaped_domain =
      domain[0] == "*" ? Regexp.escape(domain).sub("\\*", '\S*') : Regexp.escape(domain)
    domain_regex = Regexp.new("\\A#{escaped_domain}\\z", "i")

    external_domain.match(domain_regex)
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe WildcardDomainChecker do
  describe ".check_domain" do
    context "when domain is valid" do
      it "returns correct domain" do
        result1 =
          WildcardDomainChecker.check_domain(
            "*.discourse.org",
            "anything.is.possible.discourse.org",
          )
        expect(result1[0]).to eq("anything.is.possible.discourse.org")

        result2 = WildcardDomainChecker.check_domain("www.discourse.org", "www.discourse.org")
        expect(result2[0]).to eq("www.discourse.org")

        result3 = WildcardDomainChecker.check_domain("*", "hello.discourse.org")
        expect(result3[0]).to eq("hello.discourse.org")
      end
    end

    context "when domain is invalid" do
      it "doesn't return the domain" do
        result1 =
          WildcardDomainChecker.check_domain("*.discourse.org", "bad-domain.discourse.org.evil.com")
        expect(result1).to eq(nil)

        result2 =
          WildcardDomainChecker.check_domain("www.discourse.org", "www.discourse.org.evil.com")
        expect(result2).to eq(nil)

        result3 = WildcardDomainChecker.check_domain("www.discourse.org", "www.www.discourse.org")
        expect(result3).to eq(nil)

        result4 = WildcardDomainChecker.check_domain("www.*.discourse.org", "www.www.discourse.org")
        expect(result4).to eq(nil)

        result5 =
          WildcardDomainChecker.check_domain(
            "www.discourse.org",
            "www.discourse.org\nwww.discourse.org.evil.com",
          )
        expect(result5).to eq(nil)
      end
    end
  end
end


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

# GroupMessage sends a private message to a group.
# It will also avoid sending the same message repeatedly, which can happen with
# notifications to moderators when spam is detected.
#
# Options:
#
#   user: (User) If the message is about a user, pass the user object.
#   limit_once_per: (seconds) Limit sending the given type of message once every X seconds.
#                   The default is 24 hours. Set to false to always send the message.

class GroupMessage
  include Rails.application.routes.url_helpers

  RECENT_MESSAGE_PERIOD = 3.months

  def self.create(group_name, message_type, opts = {})
    GroupMessage.new(group_name, message_type, opts).create
  end

  def initialize(group_name, message_type, opts = {})
    @group_name = group_name
    @message_type = message_type
    @opts = opts
  end

  def create
    return false if sent_recently?

    post =
      PostCreator.create(
        Discourse.system_user,
        target_group_names: [@group_name],
        archetype: Archetype.private_message,
        subtype: TopicSubtype.system_message,
        title: I18n.t("system_messages.#{@message_type}.subject_template", message_params),
        raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params),
      )
    remember_message_sent
    post
  end

  def delete_previous!(respect_sent_recently: true, match_raw: true)
    return false if respect_sent_recently && sent_recently?

    posts =
      Post
        .joins(topic: { topic_allowed_groups: :group })
        .where(
          topic: {
            posts_count: 1,
            user_id: Discourse.system_user,
            archetype: Archetype.private_message,
            subtype: TopicSubtype.system_message,
            title: I18n.t("system_messages.#{@message_type}.subject_template", message_params),
            topic_allowed_groups: {
              groups: {
                name: @group_name,
              },
            },
          },
        )
        .where("posts.created_at > ?", RECENT_MESSAGE_PERIOD.ago)

    if match_raw
      posts =
        posts.where(
          raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params).rstrip,
        )
    end

    posts.find_each { |post| PostDestroyer.new(Discourse.system_user, post).destroy }
  end

  def message_params
    @message_params ||=
      begin
        h = { base_url: Discourse.base_url }.merge(@opts[:message_params] || {})
        if @opts[:user]
          h.merge!(username: @opts[:user].username, user_url: user_path(@opts[:user].username))
        end
        h
      end
  end

  def sent_recently?
    return false if @opts[:limit_once_per] == false
    Discourse.redis.get(sent_recently_key).present?
  end

  # default is to send no more than once every 24 hours (24 * 60 * 60 = 86,400 seconds)
  def remember_message_sent
    unless @opts[:limit_once_per] == false
      Discourse.redis.setex(sent_recently_key, @opts[:limit_once_per].try(:to_i) || 86_400, 1)
    end
  end

  def sent_recently_key
    "grpmsg:#{@group_name}:#{@message_type}:#{@opts[:user] ? @opts[:user].username : ""}"
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe GroupMessage do
  subject(:send_group_message) do
    GroupMessage.create(moderators_group, :user_automatically_silenced, user: user)
  end

  let(:moderators_group) { Group[:moderators].name }

  let!(:admin) { Fabricate.build(:admin, id: 999) }
  let!(:user) { Fabricate.build(:user, id: 111) }

  before { Discourse.stubs(:system_user).returns(admin) }

  describe "not sent recently" do
    before { GroupMessage.any_instance.stubs(:sent_recently?).returns(false) }

    it "should send a private message to the given group" do
      PostCreator
        .expects(:create)
        .with do |from_user, opts|
          from_user.id == (admin.id) && opts[:target_group_names] &&
            opts[:target_group_names].include?(Group[:moderators].name) &&
            opts[:archetype] == (Archetype.private_message) && opts[:title].present? &&
            opts[:raw].present?
        end
        .returns(stub_everything)
      send_group_message
    end

    it "returns whatever PostCreator returns" do
      the_output = stub_everything
      PostCreator.stubs(:create).returns(the_output)
      expect(send_group_message).to eq(the_output)
    end

    it "remembers that it was sent so it doesn't spam the group with the same message" do
      PostCreator.stubs(:create).returns(stub_everything)
      GroupMessage.any_instance.expects(:remember_message_sent)
      send_group_message
    end
  end

  describe "sent recently" do
    subject(:group_message) do
      GroupMessage.create(moderators_group, :user_automatically_silenced, user: user)
    end

    before { GroupMessage.any_instance.stubs(:sent_recently?).returns(true) }

    it { is_expected.to eq(false) }

    it "should not send the same notification again" do
      PostCreator.expects(:create).never
      group_message
    end
  end

  describe "message_params" do
    let(:user) { Fabricate.build(:user, id: 123_123) }
    shared_examples "common message params for group messages" do
      it "returns the correct params" do
        expect(message_params[:username]).to eq(user.username)
        expect(message_params[:user_url]).to be_present
      end
    end

    context "with user_automatically_silenced" do
      subject(:message_params) do
        GroupMessage.new(moderators_group, :user_automatically_silenced, user: user).message_params
      end

      include_examples "common message params for group messages"
    end

    context "with spam_post_blocked" do
      subject(:message_params) do
        GroupMessage.new(moderators_group, :spam_post_blocked, user: user).message_params
      end

      include_examples "common message params for group messages"
    end
  end

  describe "methods that use redis" do
    subject(:group_message) do
      GroupMessage.new(moderators_group, :user_automatically_silenced, user: user)
    end

    let(:user) { Fabricate.build(:user, id: 123_123) }

    before do
      PostCreator.stubs(:create).returns(stub_everything)
      group_message.stubs(:sent_recently_key).returns("the_key")
    end

    describe "sent_recently?" do
      it "returns true if redis says so" do
        Discourse.redis.stubs(:get).with(group_message.sent_recently_key).returns("1")
        expect(group_message.sent_recently?).to be_truthy
      end

      it "returns false if redis returns nil" do
        Discourse.redis.stubs(:get).with(group_message.sent_recently_key).returns(nil)
        expect(group_message.sent_recently?).to be_falsey
      end

      it "always returns false if limit_once_per is false" do
        gm =
          GroupMessage.new(
            moderators_group,
            :user_automatically_silenced,
            user: user,
            limit_once_per: false,
          )
        gm.stubs(:sent_recently_key).returns("the_key")
        Discourse.redis.stubs(:get).with(gm.sent_recently_key).returns("1")
        expect(gm.sent_recently?).to be_falsey
      end
    end

    describe "remember_message_sent" do
      it "stores a key in redis that expires after 24 hours" do
        Discourse
          .redis
          .expects(:setex)
          .with(group_message.sent_recently_key, 24 * 60 * 60, anything)
          .returns("OK")
        group_message.remember_message_sent
      end

      it "can use a given expiry time" do
        Discourse.redis.expects(:setex).with(anything, 30 * 60, anything).returns("OK")
        GroupMessage.new(
          moderators_group,
          :user_automatically_silenced,
          user: user,
          limit_once_per: 30.minutes,
        ).remember_message_sent
      end

      it "can be disabled" do
        Discourse.redis.expects(:setex).never
        GroupMessage.new(
          moderators_group,
          :user_automatically_silenced,
          user: user,
          limit_once_per: false,
        ).remember_message_sent
      end
    end
  end
end