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

class UsernameChanger
  def initialize(user, new_username, actor = nil)
    @user = user
    @old_username = user.username
    @new_username = new_username
    @actor = actor
  end

  def self.change(user, new_username, actor = nil)
    self.new(user, new_username, actor).change
  end

  def self.override(user, new_username)
    if user.username_equals_to?(new_username)
      # override anyway since case could've been changed:
      UsernameChanger.change(user, new_username, user)
      true
    elsif user.username != UserNameSuggester.fix_username(new_username)
      suggested_username = UserNameSuggester.suggest(new_username, current_username: user.username)
      UsernameChanger.change(user, suggested_username, user)
      true
    else
      false
    end
  end

  def change(asynchronous: true, run_update_job: true)
    return false if @user.username == @new_username

    @user.username = @new_username

    if @user.save
      if @actor && @old_username != @new_username
        StaffActionLogger.new(@actor).log_username_change(@user, @old_username, @new_username)
      end

      if run_update_job
        UsernameChanger.update_username(
          user_id: @user.id,
          old_username: @old_username,
          new_username: @new_username,
          avatar_template: @user.avatar_template_url,
          asynchronous: asynchronous,
        )
      end
      return true
    end

    false
  end

  def self.update_username(
    user_id:,
    old_username:,
    new_username:,
    avatar_template:,
    asynchronous: true
  )
    args = {
      user_id: user_id,
      old_username: old_username,
      new_username: new_username,
      avatar_template: avatar_template,
    }

    if asynchronous
      Jobs.enqueue(:update_username, args)
    else
      Jobs::UpdateUsername.new.execute(args)
    end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe UsernameChanger do
  before { Jobs.run_immediately! }

  describe "#change" do
    let(:user) { Fabricate(:user) }

    context "when everything goes well" do
      let!(:old_username) { user.username }

      it "should change the username" do
        new_username = "#{user.username}1234"

        events =
          DiscourseEvent
            .track_events { @result = UsernameChanger.change(user, new_username) }
            .last(2)

        expect(@result).to eq(true)

        event = events.first
        expect(event[:event_name]).to eq(:username_changed)
        expect(event[:params].first).to eq(old_username)
        expect(event[:params].second).to eq(new_username)

        event = events.last
        expect(event[:event_name]).to eq(:user_updated)
        expect(event[:params].first).to eq(user)

        user.reload
        expect(user.username).to eq(new_username)
        expect(user.username_lower).to eq(new_username.downcase)
      end

      it "do nothing if the new username is the same" do
        new_username = user.username

        events =
          DiscourseEvent.track_events { @result = UsernameChanger.change(user, new_username) }

        expect(@result).to eq(false)
        expect(events.count).to be_zero
      end
    end

    context "when something goes wrong" do
      let(:wrong_username) { "" }
      let(:username_before_change) { user.username }
      let(:username_lower_before_change) { user.username_lower }

      it "should not change the username" do
        @result = UsernameChanger.change(user, wrong_username)
        expect(@result).to eq(false)

        user.reload
        expect(user.username).to eq(username_before_change)
        expect(user.username_lower).to eq(username_lower_before_change)
      end
    end

    context "when changing the case of my username" do
      let!(:myself) { Fabricate(:user, username: "hansolo") }

      it "should change the username" do
        expect do
          expect(UsernameChanger.change(myself, "HanSolo", myself)).to eq(true)
        end.to change { UserHistory.count }.by(1)

        expect(UserHistory.last.action).to eq(UserHistory.actions[:change_username])

        expect(myself.reload.username).to eq("HanSolo")

        expect do UsernameChanger.change(myself, "HanSolo", myself) end.not_to change {
          UserHistory.count
        } # make sure it does not log a dupe

        expect do UsernameChanger.change(myself, user.username, myself) end.not_to change {
          UserHistory.count
        } # does not log if the username already exists
      end
    end

    describe "allow custom minimum username length from site settings" do
      before do
        @custom_min = 2
        SiteSetting.min_username_length = @custom_min
      end

      it "should allow a shorter username than default" do
        result = UsernameChanger.change(user, "a" * @custom_min)
        expect(result).not_to eq(false)
      end

      it "should not allow a shorter username than limit" do
        result = UsernameChanger.change(user, "a" * (@custom_min - 1))
        expect(result).to eq(false)
      end

      it "should not allow a longer username than limit" do
        result = UsernameChanger.change(user, "a" * (User.username_length.end + 1))
        expect(result).to eq(false)
      end
    end

    context "when there are posts and revisions" do
      let(:user) { Fabricate(:user, username: "foo") }
      let(:topic) { Fabricate(:topic, user: user) }

      before do
        UserActionManager.enable
        Discourse.expects(:warn_exception).never
      end

      def create_post_and_change_username(args = {}, &block)
        stub_image_size
        post = create_post(args.merge(topic_id: topic.id))

        args
          .delete(:revisions)
          &.each { |revision| post.revise(post.user, revision, force_new_version: true) }

        block.call(post) if block

        UsernameChanger.change(user, args[:target_username] || "bar")
        post.reload
      end

      context "when there are mentions" do
        it "rewrites cooked correctly" do
          post = create_post_and_change_username(raw: "Hello @foo")
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>))

          post.rebake!
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>))
        end

        it "removes the username from the search index" do
          SearchIndexer.enable
          create_post_and_change_username(raw: "Hello @foo")

          results = Search.execute("foo", min_search_term_length: 1)
          expect(results.posts).to be_empty
        end

        it "ignores case when replacing mentions" do
          post = create_post_and_change_username(raw: "There's no difference between @foo and @Foo")

          expect(post.raw).to eq("There's no difference between @bar and @bar")
          expect(post.cooked).to eq(
            %Q(<p>There’s no difference between <a class="mention" href="/u/bar">@bar</a> and <a class="mention" href="/u/bar">@bar</a></p>),
          )
        end

        it "replaces mentions when there are leading symbols" do
          post = create_post_and_change_username(raw: ".@foo -@foo %@foo _@foo ,@foo ;@foo @@foo")

          expect(post.raw).to eq(".@bar -@bar %@bar _@bar ,@bar ;@bar @@bar")
          expect(post.cooked).to match_html <<~HTML
            <p>.<a class="mention" href="/u/bar">@bar</a>
               -<a class="mention" href="/u/bar">@bar</a>
               %<a class="mention" href="/u/bar">@bar</a>
               _<a class="mention" href="/u/bar">@bar</a>
               ,<a class="mention" href="/u/bar">@bar</a>
               ;<a class="mention" href="/u/bar">@bar</a>
               @<a class="mention" href="/u/bar">@bar</a></p>
          HTML
        end

        it "replaces mentions within double and single quotes" do
          post = create_post_and_change_username(raw: %Q("@foo" '@foo'))

          expect(post.raw).to eq(%Q("@bar" '@bar'))
          expect(post.cooked).to eq(
            %Q(<p>“<a class="mention" href="/u/bar">@bar</a>” ‘<a class="mention" href="/u/bar">@bar</a>’</p>),
          )
        end

        it "replaces Markdown formatted mentions" do
          post = create_post_and_change_username(raw: "**@foo** *@foo* _@foo_ ~~@foo~~")

          expect(post.raw).to eq("**@bar** *@bar* _@bar_ ~~@bar~~")
          expect(post.cooked).to match_html <<~HTML
            <p><strong><a class="mention" href="/u/bar">@bar</a></strong>
               <em><a class="mention" href="/u/bar">@bar</a></em>
               <em><a class="mention" href="/u/bar">@bar</a></em>
               <s><a class="mention" href="/u/bar">@bar</a></s></p>
          HTML
        end

        it "replaces mentions when there are trailing symbols" do
          post = create_post_and_change_username(raw: "@foo. @foo, @foo: @foo; @foo_ @foo-")

          expect(post.raw).to eq("@bar. @bar, @bar: @bar; @bar_ @bar-")
          expect(post.cooked).to match_html <<~HTML
            <p><a class="mention" href="/u/bar">@bar</a>.
               <a class="mention" href="/u/bar">@bar</a>,
               <a class="mention" href="/u/bar">@bar</a>:
               <a class="mention" href="/u/bar">@bar</a>;
               <a class="mention" href="/u/bar">@bar</a>_
               <a class="mention" href="/u/bar">@bar</a>-</p>
          HTML
        end

        it "does not replace mention in cooked when mention contains a trailing underscore" do
          # Older versions of Discourse detected a trailing underscore as part of a username.
          # That doesn't happen anymore, so we need to do create the `cooked` for this test manually.
          post =
            create_post_and_change_username(raw: "@foobar @foo") do |p|
              p.update_columns(
                raw: p.raw.gsub("@foobar", "@foo_"),
                cooked: p.cooked.gsub("@foobar", "@foo_"),
              )
            end

          expect(post.raw).to eq("@bar_ @bar")
          expect(post.cooked).to eq(
            %Q(<p><span class="mention">@foo_</span> <a class="mention" href="/u/bar">@bar</a></p>),
          )
        end

        it "does not replace mentions when there are leading alphanumeric chars" do
          post = create_post_and_change_username(raw: "@foo a@foo 2@foo")

          expect(post.raw).to eq("@bar a@foo 2@foo")
          expect(post.cooked).to eq(
            %Q(<p><a class="mention" href="/u/bar">@bar</a> a@foo 2@foo</p>),
          )
        end

        it "does not replace username within email address" do
          post = create_post_and_change_username(raw: "@foo mail@foo.com")

          expect(post.raw).to eq("@bar mail@foo.com")
          expect(post.cooked).to eq(
            %Q(<p><a class="mention" href="/u/bar">@bar</a> <a href="mailto:mail@foo.com">mail@foo.com</a></p>),
          )
        end

        it "does not replace username in a mention of a similar username" do
          Fabricate(:user, username: "foobar")
          Fabricate(:user, username: "foo-bar")
          Fabricate(:user, username: "foo_bar")
          Fabricate(:user, username: "foo1")

          post = create_post_and_change_username(raw: "@foo @foobar @foo-bar @foo_bar @foo1")

          expect(post.raw).to eq("@bar @foobar @foo-bar @foo_bar @foo1")
          expect(post.cooked).to match_html <<~HTML
            <p><a class="mention" href="/u/bar">@bar</a> <a class="mention" href="/u/foobar">@foobar</a> <a class="mention" href="/u/foo-bar">@foo-bar</a> <a class="mention" href="/u/foo_bar">@foo_bar</a> <a class="mention" href="/u/foo1">@foo1</a></p>
          HTML
        end

        it "updates the path to the user even when it links to /user instead of /u" do
          post = create_post_and_change_username(raw: "Hello @foo")
          post.update_column(:cooked, post.cooked.gsub("/u/foo", "/users/foo"))

          expect(post.raw).to eq("Hello @bar")
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>))
        end

        it "replaces mentions within revisions" do
          revisions = [
            { raw: "Hello Foo" },
            { title: "new topic title" },
            { raw: "Hello @foo!" },
            { raw: "Hello @foo!!" },
          ]
          post = create_post_and_change_username(raw: "Hello @foo", revisions: revisions)

          expect(post.raw).to eq("Hello @bar!!")
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!!</p>))

          expect(post.revisions.count).to eq(4)

          expect(post.revisions[0].modifications["raw"][0]).to eq("Hello @bar")
          expect(post.revisions[0].modifications["raw"][1]).to eq("Hello Foo")
          expect(post.revisions[0].modifications["cooked"][0]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>),
          )
          expect(post.revisions[0].modifications["cooked"][1]).to eq("<p>Hello Foo</p>")

          expect(post.revisions[1].modifications).to include("title")

          expect(post.revisions[2].modifications["raw"][0]).to eq("Hello Foo")
          expect(post.revisions[2].modifications["raw"][1]).to eq("Hello @bar!")
          expect(post.revisions[2].modifications["cooked"][0]).to eq("<p>Hello Foo</p>")
          expect(post.revisions[2].modifications["cooked"][1]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!</p>),
          )

          expect(post.revisions[3].modifications["raw"][0]).to eq("Hello @bar!")
          expect(post.revisions[3].modifications["raw"][1]).to eq("Hello @bar!!")
          expect(post.revisions[3].modifications["cooked"][0]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!</p>),
          )
          expect(post.revisions[3].modifications["cooked"][1]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!!</p>),
          )
        end

        it "replaces mentions in posts marked for deletion" do
          post =
            create_post_and_change_username(raw: "Hello @foo") do |p|
              PostDestroyer.new(p.user, p).destroy
            end

          expect(post.raw).to_not include("@foo")
          expect(post.cooked).to_not include("foo")
          expect(post.revisions.count).to eq(1)

          expect(post.revisions[0].modifications["raw"][0]).to eq("Hello @bar")
          expect(post.revisions[0].modifications["cooked"][0]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>),
          )
        end

        it "works when users are mentioned with HTML" do
          post =
            create_post_and_change_username(
              raw: '<a class="mention">@foo</a> and <a class="mention">@someuser</a>',
            )

          expect(post.raw).to eq('<a class="mention">@bar</a> and <a class="mention">@someuser</a>')
          expect(post.cooked).to match_html(
            '<p><a class="mention">@bar</a> and <a class="mention">@someuser</a></p>',
          )
        end

        it "replaces mentions of oneself in posts" do
          post = create_post_and_change_username(raw: "Hello @#{user.username}", user: user)

          expect(post.raw).to eq("Hello @bar")
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>))
        end

        it "replaces mentions of oneself in revisions" do
          revisions = [
            { raw: "Hello Foo" },
            { title: "new topic title" },
            { raw: "Hello @#{user.username}!" },
            { raw: "Hello @#{user.username}!!" },
          ]

          post =
            create_post_and_change_username(raw: "Hello @#{user.username}", revisions: revisions)

          expect(post.raw).to eq("Hello @bar!!")
          expect(post.cooked).to eq(%Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!!</p>))

          expect(post.revisions.count).to eq(4)

          expect(post.revisions[0].modifications["raw"][0]).to eq("Hello @bar")
          expect(post.revisions[0].modifications["cooked"][0]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a></p>),
          )
          expect(post.revisions[0].modifications["cooked"][1]).to eq("<p>Hello Foo</p>")

          expect(post.revisions[1].modifications).to include("title")

          expect(post.revisions[2].modifications["raw"][0]).to eq("Hello Foo")
          expect(post.revisions[2].modifications["raw"][1]).to eq("Hello @bar!")
          expect(post.revisions[2].modifications["cooked"][0]).to eq("<p>Hello Foo</p>")
          expect(post.revisions[2].modifications["cooked"][1]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!</p>),
          )

          expect(post.revisions[3].modifications["raw"][0]).to eq("Hello @bar!")
          expect(post.revisions[3].modifications["raw"][1]).to eq("Hello @bar!!")
          expect(post.revisions[3].modifications["cooked"][0]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!</p>),
          )
          expect(post.revisions[3].modifications["cooked"][1]).to eq(
            %Q(<p>Hello <a class="mention" href="/u/bar">@bar</a>!!</p>),
          )
        end

        context "when using Unicode usernames" do
          before { SiteSetting.unicode_usernames = true }
          let(:user) { Fabricate(:user, username: "թռչուն") }

          it "it correctly updates mentions" do
            post = create_post_and_change_username(raw: "Hello @թռչուն", target_username: "птица")

            expect(post.raw).to eq("Hello @птица")
            expect(post.cooked).to eq(
              %Q(<p>Hello <a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a></p>),
            )
          end

          it "does not replace mentions when there are leading alphanumeric chars" do
            post =
              create_post_and_change_username(
                raw: "Hello @թռչուն 鳥@թռչուն 2@թռչուն ٩@թռչուն",
                target_username: "птица",
              )

            expect(post.raw).to eq("Hello @птица 鳥@թռչուն 2@թռչուն ٩@թռչուն")
            expect(post.cooked).to eq(
              %Q(<p>Hello <a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a> 鳥@թռչուն 2@թռչուն ٩@թռչուն</p>),
            )
          end

          it "does not replace username in a mention of a similar username" do
            Fabricate(:user, username: "թռչուն鳥")
            Fabricate(:user, username: "թռչուն-鳥")
            Fabricate(:user, username: "թռչուն_鳥")
            Fabricate(:user, username: "թռչուն٩")

            post =
              create_post_and_change_username(
                raw: "@թռչուն @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩",
                target_username: "птица",
              )

            expect(post.raw).to eq("@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩")
            expect(post.cooked).to match_html <<~HTML
              <p><a class="mention" href="/u/%D0%BF%D1%82%D0%B8%D1%86%D0%B0">@птица</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%E9%B3%A5">@թռչուն鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6-%E9%B3%A5">@թռչուն-鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6_%E9%B3%A5">@թռչուն_鳥</a> <a class="mention" href="/u/%D5%A9%D5%BC%D5%B9%D5%B8%D6%82%D5%B6%D9%A9">@թռչուն٩</a></p>
            HTML
          end
        end
      end

      context "when there are quotes" do
        let(:quoted_post) do
          create_post(user: user, topic: topic, post_number: 1, raw: "quoted post")
        end
        let(:avatar_url) { user.avatar_template_url.gsub("{size}", "48") }

        it "replaces the username in quote tags and updates avatar" do
          post = create_post_and_change_username(raw: <<~RAW)
            Lorem ipsum

            [quote="foo, post:1, topic:#{quoted_post.topic.id}"]
            quoted post
            [/quote]

            [quote='foo']
            quoted post
            [/quote]

            [quote=foo, post:1, topic:#{quoted_post.topic.id}]
            quoted post
            [/quote]

            dolor sit amet
          RAW

          expect(post.raw).to eq(<<~RAW.strip)
            Lorem ipsum

            [quote="bar, post:1, topic:#{quoted_post.topic.id}"]
            quoted post
            [/quote]

            [quote='bar']
            quoted post
            [/quote]

            [quote=bar, post:1, topic:#{quoted_post.topic.id}]
            quoted post
            [/quote]

            dolor sit amet
          RAW

          expect(post.cooked).to match_html <<~HTML
            <p>Lorem ipsum</p>
            <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
            <div class="title">
            <div class="quote-controls"></div>
            <img loading="lazy" alt='' width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
            <blockquote>
            <p>quoted post</p>
            </blockquote>
            </aside>
            <aside class="quote no-group" data-username="bar">
            <div class="title">
            <div class="quote-controls"></div>
            <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
            <blockquote>
            <p>quoted post</p>
            </blockquote>
            </aside>
            <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
            <div class="title">
            <div class="quote-controls"></div>
            <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
            <blockquote>
            <p>quoted post</p>
            </blockquote>
            </aside>
            <p>dolor sit amet</p>
          HTML
        end

        it "replaces the username in new quote format" do
          post = create_post_and_change_username(raw: <<~RAW)
            Lorem ipsum

            [quote="Foo Bar, post:1, topic:#{quoted_post.topic.id}, username:foo"]
            quoted post
            [/quote]

            dolor sit amet
          RAW

          expect(post.raw).to eq(<<~RAW.strip)
            Lorem ipsum

            [quote="Foo Bar, post:1, topic:#{quoted_post.topic.id}, username:bar"]
            quoted post
            [/quote]

            dolor sit amet
          RAW

          expect(post.cooked).to match_html(<<~HTML)
            <p>Lorem ipsum</p>
            <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
            <div class="title">
            <div class="quote-controls"></div>
            <img loading="lazy" alt="" width="24" height="24" src="//test.localhost/letter_avatar_proxy/v4/letter/b/b77776/48.png" class="avatar"> Foo Bar:</div>
            <blockquote>
            <p>quoted post</p>
            </blockquote>
            </aside>
            <p>dolor sit amet</p>
          HTML
        end

        context "when there is a simple quote" do
          let(:raw) { <<~RAW }
              Lorem ipsum

              [quote="foo, post:1, topic:#{quoted_post.topic.id}"]
              quoted
              [/quote]
            RAW

          let(:expected_raw) { <<~RAW.strip }
              Lorem ipsum

              [quote="bar, post:1, topic:#{quoted_post.topic.id}"]
              quoted
              [/quote]
            RAW

          let(:expected_cooked) { <<~HTML.rstrip }
              <p>Lorem ipsum</p>
              <aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
              <div class="title">
              <div class="quote-controls"></div>
              <img loading="lazy" alt='' width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
              <blockquote>
              <p>quoted</p>
              </blockquote>
              </aside>
            HTML

          it "replaces the username in quote tags when the post is deleted" do
            post =
              create_post_and_change_username(raw: raw) do |p|
                PostDestroyer.new(Discourse.system_user, p).destroy
              end

            expect(post.raw).to eq(expected_raw)
            expect(post.cooked).to match_html(expected_cooked)
          end
        end
      end

      context "when there are oneboxes" do
        let(:quoted_post) do
          create_post(user: user, topic: topic, post_number: 1, raw: "quoted post")
        end
        let(:avatar_url) { user_avatar_url(user) }
        let(:evil_trout) { Fabricate(:evil_trout) }
        let(:another_quoted_post) do
          create_post(user: evil_trout, topic: topic, post_number: 2, raw: "evil post")
        end

        def protocol_relative_url(url)
          url.sub(/^https?:/, "")
        end

        def user_avatar_url(u)
          u.avatar_template_url.gsub("{size}", "48")
        end

        it "updates avatar for linked topics and posts" do
          raw = "#{quoted_post.full_url}\n#{quoted_post.topic.url}"
          post = create_post_and_change_username(raw: raw)

          expect(post.raw).to eq(raw)
          expect(post.cooked).to match_html <<~HTML
            <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
              <div class="title">
                <div class="quote-controls"></div>
                <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
                <a href="#{protocol_relative_url(quoted_post.full_url)}">#{quoted_post.topic.title}</a>
              </div>
              <blockquote>
                quoted post
              </blockquote>
            </aside>

            <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
              <div class="title">
                <div class="quote-controls"></div>
                <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
                <a href="#{protocol_relative_url(quoted_post.topic.url)}">#{quoted_post.topic.title}</a>
              </div>
              <blockquote>
                quoted post
              </blockquote>
            </aside>
          HTML
        end

        it "does not update the wrong avatar" do
          raw = "#{quoted_post.full_url}\n#{another_quoted_post.full_url}"
          post = create_post_and_change_username(raw: raw)

          expect(post.raw).to eq(raw)
          expect(post.cooked).to match_html <<~HTML
            <aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
              <div class="title">
                <div class="quote-controls"></div>
                <img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
                <a href="#{protocol_relative_url(quoted_post.full_url)}">#{quoted_post.topic.title}</a>
              </div>
              <blockquote>
                quoted post
              </blockquote>
            </aside>
            <aside class="quote" data-post="#{another_quoted_post.post_number}" data-topic="#{another_quoted_post.topic.id}">
              <div class="title">
                <div class="quote-controls"></div>
                <img loading="lazy" alt="" width="24" height="24" src="#{user_avatar_url(evil_trout)}" class="avatar">
                <a href="#{protocol_relative_url(another_quoted_post.full_url)}">#{another_quoted_post.topic.title}</a>
              </div>
              <blockquote>
                evil post
              </blockquote>
            </aside>
          HTML
        end
      end

      it "updates username in small action posts" do
        invited_by = Fabricate(:user)
        p1 = topic.add_small_action(invited_by, "invited_user", "foo")
        p2 = topic.add_small_action(invited_by, "invited_user", "foobar")
        UsernameChanger.change(user, "bar")

        expect(p1.reload.custom_fields["action_code_who"]).to eq("bar")
        expect(p2.reload.custom_fields["action_code_who"]).to eq("foobar")
      end
    end

    context "when there are notifications" do
      def create_notification(type, notified_user, post, data = {})
        Fabricate(
          :notification,
          notification_type: Notification.types[type],
          user: notified_user,
          data: data.to_json,
          topic: post&.topic,
          post_number: post&.post_number,
        )
      end

      def notification_data(notification)
        JSON.parse(notification.reload.data, symbolize_names: true)
      end

      def original_and_display_username(username)
        { original_username: username, display_username: username, foo: "bar" }
      end

      def original_username_and_some_text_as_display_username(username)
        { original_username: username, display_username: "some text", foo: "bar" }
      end

      def only_display_username(username)
        { display_username: username }
      end

      def username_and_something_else(username)
        { username: username, foo: "bar" }
      end

      it "replaces usernames in notifications" do
        renamed_user = Fabricate(:user, username: "alice")
        another_user = Fabricate(:user, username: "another_user")
        notified_user = Fabricate(:user)
        p1 = Fabricate(:post, post_number: 1, user: renamed_user)
        p2 = Fabricate(:post, post_number: 1, user: another_user)
        Fabricate(
          :invited_user,
          invite: Fabricate(:invite, invited_by: notified_user),
          user: renamed_user,
        )
        Fabricate(
          :invited_user,
          invite: Fabricate(:invite, invited_by: notified_user),
          user: another_user,
        )

        n01 =
          create_notification(:mentioned, notified_user, p1, original_and_display_username("alice"))
        n02 =
          create_notification(
            :mentioned,
            notified_user,
            p2,
            original_and_display_username("another_user"),
          )
        n03 =
          create_notification(
            :mentioned,
            notified_user,
            p1,
            original_username_and_some_text_as_display_username("alice"),
          )
        n04 = create_notification(:mentioned, notified_user, p1, only_display_username("alice"))
        n05 =
          create_notification(:invitee_accepted, notified_user, nil, only_display_username("alice"))
        n06 =
          create_notification(
            :invitee_accepted,
            notified_user,
            nil,
            only_display_username("another_user"),
          )
        n07 =
          create_notification(
            :granted_badge,
            renamed_user,
            nil,
            username_and_something_else("alice"),
          )
        n08 =
          create_notification(
            :granted_badge,
            another_user,
            nil,
            username_and_something_else("another_user"),
          )
        n09 =
          create_notification(
            :group_message_summary,
            renamed_user,
            nil,
            username_and_something_else("alice"),
          )
        n10 =
          create_notification(
            :group_message_summary,
            another_user,
            nil,
            username_and_something_else("another_user"),
          )

        UsernameChanger.change(renamed_user, "bob")

        expect(notification_data(n01)).to eq(original_and_display_username("bob"))
        expect(notification_data(n02)).to eq(original_and_display_username("another_user"))
        expect(notification_data(n03)).to eq(
          original_username_and_some_text_as_display_username("bob"),
        )
        expect(notification_data(n04)).to eq(only_display_username("bob"))
        expect(notification_data(n05)).to eq(only_display_username("bob"))
        expect(notification_data(n06)).to eq(only_display_username("another_user"))
        expect(notification_data(n07)).to eq(username_and_something_else("bob"))
        expect(notification_data(n08)).to eq(username_and_something_else("another_user"))
        expect(notification_data(n09)).to eq(username_and_something_else("bob"))
        expect(notification_data(n10)).to eq(username_and_something_else("another_user"))
      end
    end
  end

  describe "#override" do
    common_test_cases = [
      ["overrides the username if a new name is different", "john", "bill", "bill", false],
      ["does not change the username if a new name is the same", "john", "john", "john", false],
      ["overrides the username if a new name has different case", "john", "JoHN", "JoHN", false],
    ]

    context "when unicode_usernames is off" do
      before { SiteSetting.unicode_usernames = false }

      [
        *common_test_cases,
        [
          "does not change the username if a new name after unicode normalization is the same",
          "john",
          "john¥¥",
          "john",
        ],
      ].each do |testcase_name, current, new, overrode|
        it "#{testcase_name}" do
          user = Fabricate(:user, username: current)
          UsernameChanger.override(user, new)
          expect(user.username).to eq(overrode)
        end
      end

      it "overrides the username with username suggestions in case the username is already taken" do
        user = Fabricate(:user, username: "bill")
        Fabricate(:user, username: "john")

        UsernameChanger.override(user, "john")

        expect(user.username).to eq("john1")
      end
    end

    context "when unicode_usernames is on" do
      before { SiteSetting.unicode_usernames = true }

      [
        *common_test_cases,
        [
          "overrides the username if a new name after unicode normalization is different only in case",
          "lo\u0308we",
          "L\u00F6wee",
          "L\u00F6wee",
        ],
      ].each do |testcase_name, current, new, overrode|
        it "#{testcase_name}" do
          user = Fabricate(:user, username: current)
          UsernameChanger.override(user, new)
          expect(user.username).to eq(overrode)
        end
      end
    end
  end
end


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

class GroupActionLogger
  def initialize(acting_user, group)
    @acting_user = acting_user
    @group = group
  end

  def log_make_user_group_owner(target_user)
    GroupHistory.create!(
      default_params.merge(
        action: GroupHistory.actions[:make_user_group_owner],
        target_user: target_user,
      ),
    )
  end

  def log_remove_user_as_group_owner(target_user)
    GroupHistory.create!(
      default_params.merge(
        action: GroupHistory.actions[:remove_user_as_group_owner],
        target_user: target_user,
      ),
    )
  end

  def log_add_user_to_group(target_user, subject = nil)
    GroupHistory.create!(
      default_params.merge(
        action: GroupHistory.actions[:add_user_to_group],
        target_user: target_user,
        subject: subject,
      ),
    )
  end

  def log_remove_user_from_group(target_user, subject = nil)
    GroupHistory.create!(
      default_params.merge(
        action: GroupHistory.actions[:remove_user_from_group],
        target_user: target_user,
        subject: subject,
      ),
    )
  end

  def log_change_group_settings
    @group
      .previous_changes
      .except(*excluded_attributes)
      .each do |attribute_name, value|
        next if value[0].blank? && value[1].blank?

        GroupHistory.create!(
          default_params.merge(
            action: GroupHistory.actions[:change_group_setting],
            subject: attribute_name,
            prev_value: value[0],
            new_value: value[1],
          ),
        )
      end
  end

  private

  def excluded_attributes
    %i[bio_cooked updated_at created_at user_count]
  end

  def default_params
    { group: @group, acting_user: @acting_user }
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe GroupActionLogger do
  subject(:logger) { described_class.new(group_owner, group) }

  fab!(:group_owner) { Fabricate(:user) }
  fab!(:group)
  fab!(:user)

  before { group.add_owner(group_owner) }

  describe "#log_make_user_group_owner" do
    it "should create the right record" do
      logger.log_make_user_group_owner(user)

      group_history = GroupHistory.last

      expect(group_history.action).to eq(GroupHistory.actions[:make_user_group_owner])
      expect(group_history.acting_user).to eq(group_owner)
      expect(group_history.target_user).to eq(user)
    end
  end

  describe "#log_remove_user_as_group_owner" do
    it "should create the right record" do
      logger.log_remove_user_as_group_owner(user)

      group_history = GroupHistory.last

      expect(group_history.action).to eq(GroupHistory.actions[:remove_user_as_group_owner])
      expect(group_history.acting_user).to eq(group_owner)
      expect(group_history.target_user).to eq(user)
    end
  end

  describe "#log_add_user_to_group" do
    context "as a group owner" do
      it "should create the right record" do
        logger.log_add_user_to_group(user)

        group_history = GroupHistory.last

        expect(group_history.action).to eq(GroupHistory.actions[:add_user_to_group])
        expect(group_history.acting_user).to eq(group_owner)
        expect(group_history.target_user).to eq(user)
      end
    end

    context "as a normal user" do
      subject(:logger) { described_class.new(user, group) }

      before { group.update!(public_admission: true) }

      it "should create the right record" do
        logger.log_add_user_to_group(user)

        group_history = GroupHistory.last

        expect(group_history.action).to eq(GroupHistory.actions[:add_user_to_group])
        expect(group_history.acting_user).to eq(user)
        expect(group_history.target_user).to eq(user)
      end
    end
  end

  describe "#log_remove_user_from_group" do
    context "as group owner" do
      it "should create the right record" do
        logger.log_remove_user_from_group(user)

        group_history = GroupHistory.last

        expect(group_history.action).to eq(GroupHistory.actions[:remove_user_from_group])
        expect(group_history.acting_user).to eq(group_owner)
        expect(group_history.target_user).to eq(user)
      end
    end

    context "as a normal user" do
      subject(:logger) { described_class.new(user, group) }

      before { group.update!(public_exit: true) }

      it "should create the right record" do
        logger.log_remove_user_from_group(user)

        group_history = GroupHistory.last

        expect(group_history.action).to eq(GroupHistory.actions[:remove_user_from_group])
        expect(group_history.acting_user).to eq(user)
        expect(group_history.target_user).to eq(user)
      end
    end
  end

  describe "#log_change_group_settings" do
    it "should create the right record" do
      group.update!(public_admission: true, created_at: Time.zone.now)

      expect { logger.log_change_group_settings }.to change { GroupHistory.count }.by(1)

      group_history = GroupHistory.last

      expect(group_history.action).to eq(GroupHistory.actions[:change_group_setting])
      expect(group_history.acting_user).to eq(group_owner)
      expect(group_history.subject).to eq("public_admission")
      expect(group_history.prev_value).to eq("f")
      expect(group_history.new_value).to eq("t")
    end
  end
end


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

class GroupMentionsUpdater
  def self.update(current_name, previous_name)
    Post
      .where(
        "cooked LIKE '%class=\"mention-group%' AND raw LIKE :previous_name",
        previous_name: "%@#{previous_name}%",
      )
      .find_in_batches do |posts|
        posts.each do |post|
          post.raw.gsub!(/(^|\s)(@#{previous_name})(\s|$)/, "\\1@#{current_name}\\3")
          post.save!(validate: false)
        end
      end
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe GroupMentionsUpdater do
  fab!(:post)

  before { Jobs.run_immediately! }

  describe ".update" do
    it "should update valid group mentions" do
      new_group_name = "awesome_team"
      old_group_name = "team"

      [
        ["@#{old_group_name} is awesome!", "@#{new_group_name} is awesome!"],
        ["This @#{old_group_name} is awesome!", "This @#{new_group_name} is awesome!"],
        ["Mention us @ @#{old_group_name}", "Mention us @ @#{new_group_name}"],
      ].each do |raw, expected_raw|
        group =
          Fabricate(:group, name: old_group_name, mentionable_level: Group::ALIAS_LEVELS[:everyone])

        post.update!(raw: raw)
        group.update!(name: new_group_name)
        post.reload

        expect(post.raw_mentions).to eq([new_group_name])
        expect(post.raw).to eq(expected_raw)

        group.destroy!
      end
    end

    it "should not update invalid group mentions" do
      group = Fabricate(:group, name: "team", mentionable_level: Group::ALIAS_LEVELS[:everyone])

      post.update!(raw: "This is not valid@team.com")

      expect(post.reload.raw_mentions).to eq([])

      group.update!(name: "new_team_name")

      expect(post.reload.raw_mentions).to eq([])
    end

    it "should ignore validations" do
      everyone_mention_level = Group::ALIAS_LEVELS[:everyone]

      %w[awesome_team pro_team].each do |name|
        Fabricate(:group, name: name, mentionable_level: everyone_mention_level)
      end

      post.update!(raw: "@awesome_team is cool and so is @pro_team")

      SiteSetting.max_mentions_per_post = 1
      GroupMentionsUpdater.update("cool_team", "awesome_team")

      post.reload
      expect(post.raw_mentions).to match_array(%w[cool_team pro_team])
    end
  end
end


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

class PostActionNotifier
  def self.disable
    @disabled = true
  end

  def self.enable
    @disabled = false
  end

  # For testing purposes
  def self.reset!
    @custom_post_revision_notifier_recipients = nil
  end

  def self.alerter
    @alerter ||= PostAlerter.new
  end

  def self.refresh_like_notification(post, read)
    return unless post && post.user_id && post.topic

    usernames =
      post
        .post_actions
        .where(post_action_type_id: PostActionType.types[:like])
        .joins(:user)
        .order("post_actions.created_at desc")
        .where("post_actions.created_at > ?", 1.day.ago)
        .pluck(:username)

    if usernames.length > 0
      data = {
        topic_title: post.topic.title,
        username: usernames[0],
        display_username: usernames[0],
        username2: usernames[1],
        count: usernames.length,
      }
      Notification.create(
        notification_type: Notification.types[:liked],
        topic_id: post.topic_id,
        post_number: post.post_number,
        user_id: post.user_id,
        read: read,
        data: data.to_json,
      )
    end
  end

  def self.post_action_deleted(post_action)
    return if @disabled

    # We only care about deleting post actions for now
    return if post_action.deleted_at.blank?

    if post_action.post_action_type_id == PostActionType.types[:like] && post_action.post
      read = true

      Notification
        .where(
          topic_id: post_action.post.topic_id,
          user_id: post_action.post.user_id,
          post_number: post_action.post.post_number,
          notification_type: Notification.types[:liked],
        )
        .each do |notification|
          read = false unless notification.read
          notification.destroy
        end

      refresh_like_notification(post_action.post, read)
    else
      # not using destroy_all cause we want stuff to trigger
      Notification.where(post_action_id: post_action.id).each(&:destroy)
    end
  end

  def self.post_action_created(post_action)
    return if @disabled

    # We only notify on likes for now
    return unless post_action.is_like?

    post = post_action.post
    return if post_action.user.blank? || post.blank?

    alerter.create_notification(
      post.user,
      Notification.types[:liked],
      post,
      display_username: post_action.user.username,
      post_action_id: post_action.id,
      user_id: post_action.user_id,
    )
  end

  def self.after_create_post_revision(post_revision)
    return if @disabled

    post = post_revision.post

    return unless post
    return if post_revision.user.blank?
    return if post.topic.blank?
    return if post.topic.private_message?
    return if notification_is_disabled?(post_revision)

    user_ids = []

    user_ids << post.user_id if post_revision.user_id != post.user_id

    # Notify all users watching the topic when the OP of a wiki topic is edited
    # or if the topic category allows unlimited owner edits on the OP.
    if post.is_first_post? &&
         (post.wiki? || post.topic.category_allows_unlimited_owner_edits_on_first_post?)
      user_ids.concat(
        TopicUser
          .watching(post.topic_id)
          .where.not(user_id: post_revision.user_id)
          .where(topic: post.topic)
          .pluck(:user_id),
      )
    end

    custom_post_revision_notifier_recipients.each do |block|
      user_ids.concat(Array(block.call(post_revision)))
    end

    if user_ids.present?
      DB.after_commit do
        Jobs.enqueue(:notify_post_revision, user_ids: user_ids, post_revision_id: post_revision.id)
      end
    end
  end

  def self.after_post_unhide(post, flaggers)
    return if @disabled || post.last_editor.blank? || flaggers.blank?

    flaggers.each do |flagger|
      alerter.create_notification(
        flagger,
        Notification.types[:edited],
        post,
        display_username: post.last_editor.username,
        acting_user_id: post.last_editor.id,
      )
    end
  end

  def self.custom_post_revision_notifier_recipients
    @custom_post_revision_notifier_recipients ||= Set.new
  end

  def self.add_post_revision_notifier_recipients(&block)
    custom_post_revision_notifier_recipients << block
  end

  private

  def self.notification_is_disabled?(post_revision)
    modifications = post_revision.modifications
    (
      SiteSetting.disable_system_edit_notifications &&
        post_revision.user_id == Discourse::SYSTEM_USER_ID
    ) ||
      (
        SiteSetting.disable_category_edit_notifications &&
          modifications&.dig("category_id").present?
      ) || (SiteSetting.disable_tags_edit_notifications && modifications&.dig("tags").present?)
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe PostActionNotifier do
  before do
    PostActionNotifier.enable
    Jobs.run_immediately!
  end

  fab!(:evil_trout)
  fab!(:post)

  context "when editing a post" do
    it "notifies a user of the revision" do
      expect { post.revise(evil_trout, raw: "world is the new body of the message") }.to change {
        post.reload.user.notifications.count
      }.by(1)
    end

    it "notifies watching users of revision when post is wiki-ed and first post in topic" do
      SiteSetting.editing_grace_period_max_diff = 1

      post.update!(wiki: true)
      owner = post.user
      user2 = Fabricate(:user)
      user3 = Fabricate(:user)

      TopicUser.change(
        user2.id,
        post.topic,
        notification_level: TopicUser.notification_levels[:watching],
      )

      TopicUser.change(
        user3.id,
        post.topic,
        notification_level: TopicUser.notification_levels[:tracking],
      )

      expect do
        post.revise(Fabricate(:user), raw: "I made some changes to the wiki!")
      end.to change { Notification.count }.by(2)

      edited_notification_type = Notification.types[:edited]

      expect(Notification.exists?(user: owner, notification_type: edited_notification_type)).to eq(
        true,
      )

      expect(Notification.exists?(user: user2, notification_type: edited_notification_type)).to eq(
        true,
      )

      expect do post.revise(owner, raw: "I made some changes to the wiki again!") end.to change {
        Notification.where(notification_type: edited_notification_type).count
      }.by(1)

      expect(
        Notification.where(user: user2, notification_type: edited_notification_type).count,
      ).to eq(2)

      expect do post.revise(user2, raw: "I changed the wiki totally") end.to change {
        Notification.where(notification_type: edited_notification_type).count
      }.by(1)

      expect(
        Notification.where(user: owner, notification_type: edited_notification_type).count,
      ).to eq(2)
    end

    it "notifies watching users of revision when topic category allow_unlimited_owner_edits_on_first_post and first post in topic is edited" do
      SiteSetting.editing_grace_period_max_diff = 1

      post.topic.update(
        category: Fabricate(:category, allow_unlimited_owner_edits_on_first_post: true),
      )
      owner = post.user
      user2 = Fabricate(:user)
      user3 = Fabricate(:user)

      TopicUser.change(
        user2.id,
        post.topic,
        notification_level: TopicUser.notification_levels[:watching],
      )

      TopicUser.change(
        user3.id,
        post.topic,
        notification_level: TopicUser.notification_levels[:tracking],
      )

      expect do
        post.revise(Fabricate(:user), raw: "I made some changes to the first post!")
      end.to change { Notification.count }.by(2)

      edited_notification_type = Notification.types[:edited]

      expect(Notification.exists?(user: owner, notification_type: edited_notification_type)).to eq(
        true,
      )

      expect(Notification.exists?(user: user2, notification_type: edited_notification_type)).to eq(
        true,
      )

      expect do
        post.revise(owner, raw: "I made some changes to the first post again!")
      end.to change { Notification.where(notification_type: edited_notification_type).count }.by(1)

      expect(
        Notification.where(user: user2, notification_type: edited_notification_type).count,
      ).to eq(2)

      expect do post.revise(user2, raw: "I changed the first post totally") end.to change {
        Notification.where(notification_type: edited_notification_type).count
      }.by(1)

      expect(
        Notification.where(user: owner, notification_type: edited_notification_type).count,
      ).to eq(2)
    end

    it "stores the revision number with the notification" do
      post.revise(evil_trout, raw: "world is the new body of the message")
      notification_data = JSON.parse post.user.notifications.last.data
      expect(notification_data["revision_number"]).to eq post.post_revisions.last.number
    end

    context "when edit notifications are disabled" do
      before { SiteSetting.disable_system_edit_notifications = true }

      it "notifies a user of the revision made by another user" do
        expect { post.revise(evil_trout, raw: "world is the new body of the message") }.to change(
          post.user.notifications,
          :count,
        ).by(1)
      end

      it "does not notify a user of the revision made by the system user" do
        expect {
          post.revise(Discourse.system_user, raw: "world is the new body of the message")
        }.not_to change(post.user.notifications, :count)
      end
    end

    context "when category edit notifications are disabled" do
      it "notifies a user of the revision made by another user" do
        SiteSetting.disable_category_edit_notifications = false

        expect { post.revise(evil_trout, category_id: Fabricate(:category).id) }.to change(
          post.user.notifications,
          :count,
        ).by(1)
      end

      it "does not notify a user of the revision made by the system user" do
        SiteSetting.disable_category_edit_notifications = true

        expect { post.revise(evil_trout, category_id: Fabricate(:category).id) }.not_to change(
          post.user.notifications,
          :count,
        )
      end
    end

    context "when tags edit notifications are disabled" do
      it "notifies a user of the revision made by another user" do
        SiteSetting.disable_tags_edit_notifications = false

        expect { post.revise(evil_trout, tags: [Fabricate(:tag).name]) }.to change(
          post.user.notifications,
          :count,
        ).by(1)
      end

      it "does not notify a user of the revision made by the system user" do
        SiteSetting.disable_tags_edit_notifications = true

        expect { post.revise(evil_trout, tags: [Fabricate(:tag).name]) }.not_to change(
          post.user.notifications,
          :count,
        )
      end
    end

    context "when using plugin API to add custom recipients" do
      let(:lurker) { Fabricate(:user) }

      before do
        plugin = Plugin::Instance.new
        plugin.add_post_revision_notifier_recipients { |post_revision| [lurker.id] }
      end

      after { PostActionNotifier.reset! }

      it "notifies the specified user of the revision" do
        expect { post.revise(evil_trout, raw: "world is the new body of the message") }.to change {
          lurker.notifications.count
        }.by(1)
      end
    end
  end

  context "with private message" do
    fab!(:private_message) do
      Fabricate(:topic, archetype: Archetype.private_message, category_id: nil)
    end
    fab!(:user)
    fab!(:mention_post) do
      Fabricate(:post, topic: private_message, user: user, raw: "Hello @eviltrout")
    end

    it "won't notify someone who can't see the post" do
      expect { PostAlerter.post_created(mention_post) }.not_to change(
        evil_trout.notifications,
        :count,
      )
    end

    it "creates like notifications" do
      other_user = Fabricate(:user)
      private_message.allowed_users << user << other_user
      expect { PostActionCreator.like(other_user, mention_post) }.to change(
        user.notifications,
        :count,
      )
    end
  end

  context "with moderator action post" do
    fab!(:user)
    fab!(:first_post) { Fabricate(:post, user: user, raw: "A useless post for you.") }
    let(:topic) { first_post.topic }

    it "should not notify anyone" do
      expect {
        Fabricate(
          :post,
          topic: topic,
          raw: "This topic is CLOSED",
          post_type: Post.types[:moderator_action],
        )
      }.to_not change { Notification.count }
    end
  end
end


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

class ColorSchemeRevisor
  def initialize(color_scheme, params = {})
    @color_scheme = color_scheme
    @params = params
  end

  def self.revise(color_scheme, params)
    self.new(color_scheme, params).revise
  end

  def revise
    ColorScheme.transaction do
      @color_scheme.name = @params[:name] if @params.has_key?(:name)
      @color_scheme.user_selectable = @params[:user_selectable] if @params.has_key?(
        :user_selectable,
      )
      @color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id)
      has_colors = @params[:colors]

      if has_colors
        @params[:colors].each do |c|
          if existing = @color_scheme.colors_by_name[c[:name]]
            existing.update(c)
          else
            @color_scheme.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex])
          end
        end
        @color_scheme.clear_colors_cache
      end

      if has_colors || @color_scheme.saved_change_to_name? ||
           @color_scheme.will_save_change_to_user_selectable? ||
           @color_scheme.saved_change_to_base_scheme_id?
        @color_scheme.save
      end
    end
    @color_scheme
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe ColorSchemeRevisor do
  let(:color) { Fabricate.build(:color_scheme_color, hex: "FFFFFF", color_scheme: nil) }
  let(:color_scheme) do
    Fabricate(
      :color_scheme,
      created_at: 1.day.ago,
      updated_at: 1.day.ago,
      color_scheme_colors: [color],
    )
  end
  let(:valid_params) { { name: color_scheme.name, colors: nil } }

  describe "revise" do
    it "does nothing if there are no changes" do
      expect {
        ColorSchemeRevisor.revise(color_scheme, valid_params.merge(colors: nil))
      }.to_not change { color_scheme.reload.updated_at }
    end

    it "can change the name" do
      ColorSchemeRevisor.revise(color_scheme, valid_params.merge(name: "Changed Name"))
      expect(color_scheme.reload.name).to eq("Changed Name")
    end

    it "can update the base_scheme_id" do
      ColorSchemeRevisor.revise(color_scheme, valid_params.merge(base_scheme_id: "test"))
      expect(color_scheme.reload.base_scheme_id).to eq("test")
    end

    it "can change colors" do
      ColorSchemeRevisor.revise(
        color_scheme,
        valid_params.merge(
          colors: [{ name: color.name, hex: "BEEF99" }, { name: "bob", hex: "AAAAAA" }],
        ),
      )
      color_scheme.reload

      expect(color_scheme.version).to eq(2)
      expect(color_scheme.colors.size).to eq(2)
      expect(color_scheme.colors.find_by(name: color.name).hex).to eq("BEEF99")
      expect(color_scheme.colors.find_by(name: "bob").hex).to eq("AAAAAA")
    end

    it "doesn't make changes when a color is invalid" do
      expect {
        cs =
          ColorSchemeRevisor.revise(
            color_scheme,
            valid_params.merge(colors: [{ name: color.name, hex: "OOPS" }]),
          )
        expect(cs).not_to be_valid
        expect(cs.errors).to be_present
      }.to_not change { color_scheme.reload.version }
      expect(color_scheme.colors.first.hex).to eq(color.hex)
    end

    it "can change the user_selectable column" do
      expect(color_scheme.user_selectable).to eq(false)

      ColorSchemeRevisor.revise(color_scheme, { user_selectable: true })
      expect(color_scheme.reload.user_selectable).to eq(true)
    end
  end
end


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

class UserUpdater
  CATEGORY_IDS = {
    watched_first_post_category_ids: :watching_first_post,
    watched_category_ids: :watching,
    tracked_category_ids: :tracking,
    regular_category_ids: :regular,
    muted_category_ids: :muted,
  }

  TAG_NAMES = {
    watching_first_post_tags: :watching_first_post,
    watched_tags: :watching,
    tracked_tags: :tracking,
    muted_tags: :muted,
  }

  OPTION_ATTR = %i[
    mailing_list_mode
    mailing_list_mode_frequency
    email_digests
    email_level
    email_messages_level
    external_links_in_new_tab
    enable_quoting
    enable_defer
    color_scheme_id
    dark_scheme_id
    dynamic_favicon
    automatically_unpin_topics
    digest_after_minutes
    new_topic_duration_minutes
    auto_track_topics_after_msecs
    notification_level_when_replying
    email_previous_replies
    email_in_reply_to
    like_notification_frequency
    include_tl0_in_digests
    theme_ids
    allow_private_messages
    enable_allowed_pm_users
    homepage_id
    hide_profile_and_presence
    text_size
    title_count_mode
    timezone
    skip_new_user_tips
    seen_popups
    default_calendar
    bookmark_auto_delete_preference
    sidebar_link_to_filtered_list
    sidebar_show_count_of_new_items
    watched_precedence_over_muted
  ]

  NOTIFICATION_SCHEDULE_ATTRS = -> do
    attrs = [:enabled]
    7.times do |n|
      attrs.push("day_#{n}_start_time".to_sym)
      attrs.push("day_#{n}_end_time".to_sym)
    end
    { user_notification_schedule: attrs }
  end.call

  def initialize(actor, user)
    @user = user
    @user_guardian = Guardian.new(user)
    @guardian = Guardian.new(actor)
    @actor = actor
  end

  def update(attributes = {})
    user_profile = user.user_profile
    user_profile.dismissed_banner_key = attributes[:dismissed_banner_key] if attributes[
      :dismissed_banner_key
    ].present?
    unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_bio
      user_profile.bio_raw = attributes.fetch(:bio_raw) { user_profile.bio_raw }
    end

    unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_location
      user_profile.location = attributes.fetch(:location) { user_profile.location }
    end

    unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_website
      user_profile.website = format_url(attributes.fetch(:website) { user_profile.website })
    end

    if attributes[:profile_background_upload_url] == "" ||
         !guardian.can_upload_profile_header?(user)
      user_profile.profile_background_upload_id = nil
    elsif upload = Upload.get_from_url(attributes[:profile_background_upload_url])
      user_profile.profile_background_upload_id = upload.id
    end

    if attributes[:card_background_upload_url] == "" ||
         !guardian.can_upload_user_card_background?(user)
      user_profile.card_background_upload_id = nil
    elsif upload = Upload.get_from_url(attributes[:card_background_upload_url])
      user_profile.card_background_upload_id = upload.id
    end

    if attributes[:user_notification_schedule]
      user_notification_schedule =
        user.user_notification_schedule || UserNotificationSchedule.new(user: user)
      user_notification_schedule.assign_attributes(attributes[:user_notification_schedule])
    end

    old_user_name = user.name.present? ? user.name : ""

    user.name = attributes.fetch(:name) { user.name } if guardian.can_edit_name?(user)

    user.locale = attributes.fetch(:locale) { user.locale }
    user.date_of_birth = attributes.fetch(:date_of_birth) { user.date_of_birth }

    if attributes[:title] && attributes[:title] != user.title &&
         guardian.can_grant_title?(user, attributes[:title])
      user.title = attributes[:title]
    end

    if SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] &&
         attributes[:primary_group_id] != user.primary_group_id &&
         guardian.can_use_primary_group?(user, attributes[:primary_group_id])
      user.primary_group_id = attributes[:primary_group_id]
    elsif SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] &&
          attributes[:primary_group_id].blank?
      user.primary_group_id = nil
    end

    if attributes[:flair_group_id] && attributes[:flair_group_id] != user.flair_group_id &&
         (
           attributes[:flair_group_id].blank? ||
             guardian.can_use_flair_group?(user, attributes[:flair_group_id])
         )
      user.flair_group_id = attributes[:flair_group_id]
    end

    if @guardian.can_change_tracking_preferences?(user)
      CATEGORY_IDS.each do |attribute, level|
        if ids = attributes[attribute]
          CategoryUser.batch_set(user, level, ids)
        end
      end

      TAG_NAMES.each do |attribute, level|
        if attributes.has_key?(attribute)
          TagUser.batch_set(user, level, attributes[attribute]&.split(",") || [])
        end
      end
    end

    save_options = false

    # special handling for theme_id cause we need to bump a sequence number
    if attributes.key?(:theme_ids)
      attributes[:theme_ids].reject!(&:blank?)
      attributes[:theme_ids].map!(&:to_i)

      if @user_guardian.allow_themes?(attributes[:theme_ids])
        user.user_option.theme_key_seq += 1 if user.user_option.theme_ids != attributes[:theme_ids]
      else
        attributes.delete(:theme_ids)
      end
    end

    if attributes.key?(:text_size)
      user.user_option.text_size_seq += 1 if user.user_option.text_size.to_s !=
        attributes[:text_size]
    end

    OPTION_ATTR.each do |attribute|
      if attributes.key?(attribute)
        save_options = true

        if [true, false].include?(user.user_option.public_send(attribute))
          val = attributes[attribute].to_s == "true"
          user.user_option.public_send("#{attribute}=", val)
        else
          user.user_option.public_send("#{attribute}=", attributes[attribute])
        end
      end
    end

    if attributes.key?(:skip_new_user_tips) && user.user_option.skip_new_user_tips
      user.user_option.seen_popups = [-1]
    end

    # automatically disable digests when mailing_list_mode is enabled
    user.user_option.email_digests = false if user.user_option.mailing_list_mode

    fields = attributes[:custom_fields]
    user.custom_fields = user.custom_fields.merge(fields) if fields.present?

    saved = nil

    User.transaction do
      update_muted_users(attributes[:muted_usernames]) if attributes.key?(:muted_usernames)

      if attributes.key?(:allowed_pm_usernames)
        update_allowed_pm_users(attributes[:allowed_pm_usernames])
      end

      if attributes.key?(:discourse_connect)
        update_discourse_connect(attributes[:discourse_connect])
      end

      if attributes.key?(:user_associated_accounts)
        updated_associated_accounts(attributes[:user_associated_accounts])
      end

      if attributes.key?(:sidebar_category_ids)
        SidebarSectionLinksUpdater.update_category_section_links(
          user,
          category_ids:
            Category
              .secured(@user_guardian)
              .where(id: attributes[:sidebar_category_ids])
              .pluck(:id),
        )
      end

      if attributes.key?(:sidebar_tag_names) && SiteSetting.tagging_enabled
        SidebarSectionLinksUpdater.update_tag_section_links(
          user,
          tag_ids:
            DiscourseTagging
              .filter_visible(Tag, @user_guardian)
              .where(name: attributes[:sidebar_tag_names])
              .pluck(:id),
        )
      end

      if SiteSetting.enable_user_status?
        update_user_status(attributes[:status]) if attributes.has_key?(:status)
      end

      name_changed = user.name_changed?
      saved =
        (!save_options || user.user_option.save) &&
          (user_notification_schedule.nil? || user_notification_schedule.save) &&
          user_profile.save && user.save

      if saved && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0)
        StaffActionLogger.new(@actor).log_name_change(
          user.id,
          old_user_name,
          attributes.fetch(:name) { "" },
        )
      end
      DiscourseEvent.trigger(:within_user_updater_transaction, user, attributes)
    rescue Addressable::URI::InvalidURIError => e
      # Prevent 500 for crazy url input
      return saved
    end

    if saved
      if user_notification_schedule
        if user_notification_schedule.enabled
          user_notification_schedule.create_do_not_disturb_timings(delete_existing: true)
        else
          user_notification_schedule.destroy_scheduled_timings
        end
      end
      if attributes.key?(:seen_popups) || attributes.key?(:skip_new_user_tips)
        MessageBus.publish(
          "/user-tips/#{user.id}",
          user.user_option.seen_popups,
          user_ids: [user.id],
        )
      end
      DiscourseEvent.trigger(:user_updated, user)
    end

    saved
  end

  def update_muted_users(usernames)
    usernames ||= ""
    desired_usernames = usernames.split(",").reject { |username| user.username == username }
    desired_ids = User.where(username: desired_usernames).pluck(:id)
    if desired_ids.empty?
      MutedUser.where(user_id: user.id).destroy_all
    else
      MutedUser.where("user_id = ? AND muted_user_id not in (?)", user.id, desired_ids).destroy_all

      # SQL is easier here than figuring out how to do the same in AR
      DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids)
        INSERT into muted_users(user_id, muted_user_id, created_at, updated_at)
        SELECT :user_id, id, :now, :now
        FROM users
        WHERE id in (:desired_ids)
        ON CONFLICT DO NOTHING
      SQL
    end
  end

  def update_allowed_pm_users(usernames)
    usernames ||= ""
    desired_usernames = usernames.split(",").reject { |username| user.username == username }
    desired_ids = User.where(username: desired_usernames).pluck(:id)

    if desired_ids.empty?
      AllowedPmUser.where(user_id: user.id).destroy_all
    else
      AllowedPmUser.where(
        "user_id = ? AND allowed_pm_user_id not in (?)",
        user.id,
        desired_ids,
      ).destroy_all

      # SQL is easier here than figuring out how to do the same in AR
      DB.exec(<<~SQL, now: Time.zone.now, user_id: user.id, desired_ids: desired_ids)
        INSERT into allowed_pm_users(user_id, allowed_pm_user_id, created_at, updated_at)
        SELECT :user_id, id, :now, :now
        FROM users
        WHERE id in (:desired_ids)
        ON CONFLICT DO NOTHING
      SQL
    end
  end

  def updated_associated_accounts(associations)
    associations.each do |association|
      user_associated_account =
        UserAssociatedAccount.find_or_initialize_by(
          user_id: user.id,
          provider_name: association[:provider_name],
        )
      if association[:provider_uid].present?
        user_associated_account.update!(provider_uid: association[:provider_uid])
      else
        user_associated_account.destroy!
      end
    end
  end

  private

  def update_user_status(status)
    if status.blank?
      @user.clear_status!
    else
      @user.set_status!(status[:description], status[:emoji], status[:ends_at])
    end
  end

  def update_discourse_connect(discourse_connect)
    external_id = discourse_connect[:external_id]
    sso = SingleSignOnRecord.find_or_initialize_by(user_id: user.id)

    if external_id.present?
      sso.update!(
        external_id: discourse_connect[:external_id],
        last_payload: "external_id=#{discourse_connect[:external_id]}",
      )
    else
      sso.destroy!
    end
  end

  attr_reader :user, :guardian

  def format_url(website)
    return nil if website.blank?
    website =~ /\Ahttp/ ? website : "http://#{website}"
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe UserUpdater do
  fab!(:user)
  fab!(:u1) { Fabricate(:user) }
  fab!(:u2) { Fabricate(:user) }
  fab!(:u3) { Fabricate(:user) }

  let(:acting_user) { Fabricate.build(:user) }

  describe "#update_muted_users" do
    it "has no cross talk" do
      updater = UserUpdater.new(u1, u1)
      updater.update_muted_users("#{u2.username},#{u3.username}")

      updater = UserUpdater.new(u2, u2)
      updater.update_muted_users("#{u3.username},#{u1.username}")

      updater = UserUpdater.new(u3, u3)
      updater.update_muted_users("")

      expect(MutedUser.where(user_id: u2.id).pluck(:muted_user_id)).to match_array([u3.id, u1.id])
      expect(MutedUser.where(user_id: u1.id).pluck(:muted_user_id)).to match_array([u2.id, u3.id])
      expect(MutedUser.where(user_id: u3.id).count).to eq(0)
    end

    it "excludes acting user" do
      updater = UserUpdater.new(u1, u1)
      updater.update_muted_users("#{u1.username},#{u2.username}")

      expect(MutedUser.where(muted_user_id: u2.id).pluck(:muted_user_id)).to match_array([u2.id])
    end
  end

  describe "#update" do
    fab!(:category)
    fab!(:tag)
    fab!(:tag2) { Fabricate(:tag) }

    it "saves user" do
      user = Fabricate(:user, name: "Billy Bob")
      updater = UserUpdater.new(user, user)

      updater.update(name: "Jim Tom")

      expect(user.reload.name).to eq "Jim Tom"
    end

    describe "the within_user_updater_transaction event" do
      it "allows plugins to perform additional updates" do
        update_attributes = { name: "Jimmmy Johnny" }
        handler =
          Proc.new do |user, attrs|
            user.user_profile.update!(bio_raw: "hello world I'm Jimmmy")
            expect(attrs).to eq(update_attributes)
          end
        DiscourseEvent.on(:within_user_updater_transaction, &handler)

        updater = UserUpdater.new(user, user)
        updater.update(update_attributes)

        expect(user.reload.name).to eq("Jimmmy Johnny")
        expect(user.user_profile.bio_raw).to eq("hello world I'm Jimmmy")
      ensure
        DiscourseEvent.off(:within_user_updater_transaction, &handler)
      end

      it "can cancel the whole update transaction if a handler raises" do
        error_class = Class.new(StandardError)
        handler = Proc.new { raise error_class.new }

        DiscourseEvent.on(:within_user_updater_transaction, &handler)

        old_name = user.name
        updater = UserUpdater.new(user, user)

        expect { updater.update(name: "Failure McClario") }.to raise_error(error_class)

        expect(user.reload.name).to eq(old_name)
      ensure
        DiscourseEvent.off(:within_user_updater_transaction, &handler)
      end
    end

    it "can update categories and tags" do
      updater = UserUpdater.new(user, user)
      updater.update(watched_tags: "#{tag.name},#{tag2.name}", muted_category_ids: [category.id])

      expect(
        TagUser.where(
          user_id: user.id,
          tag_id: tag.id,
          notification_level: TagUser.notification_levels[:watching],
        ).exists?,
      ).to eq(true)

      expect(
        TagUser.where(
          user_id: user.id,
          tag_id: tag2.id,
          notification_level: TagUser.notification_levels[:watching],
        ).exists?,
      ).to eq(true)

      expect(
        CategoryUser.where(
          user_id: user.id,
          category_id: category.id,
          notification_level: CategoryUser.notification_levels[:muted],
        ).count,
      ).to eq(1)
    end

    context "with a staged user" do
      let(:staged_user) { Fabricate(:staged) }

      context "when allow_changing_staged_user_tracking is false" do
        before { SiteSetting.allow_changing_staged_user_tracking = false }

        it "doesn't update muted categories and watched tags" do
          updater = UserUpdater.new(Fabricate(:admin), staged_user)
          updater.update(watched_tags: "#{tag.name}", muted_category_ids: [category.id])
          expect(TagUser.exists?(user_id: staged_user.id)).to eq(false)
          expect(CategoryUser.exists?(user_id: staged_user.id)).to eq(false)
        end
      end

      context "when allow_changing_staged_user_tracking is true" do
        before { SiteSetting.allow_changing_staged_user_tracking = true }

        it "updates muted categories and watched tags" do
          updater = UserUpdater.new(Fabricate(:admin), staged_user)
          updater.update(watched_tags: "#{tag.name}", muted_category_ids: [category.id])
          expect(
            TagUser.exists?(
              user_id: staged_user.id,
              tag_id: tag.id,
              notification_level: TagUser.notification_levels[:watching],
            ),
          ).to eq(true)

          expect(
            CategoryUser.exists?(
              user_id: staged_user.id,
              category_id: category.id,
              notification_level: CategoryUser.notification_levels[:muted],
            ),
          ).to eq(true)
        end
      end
    end

    it "doesn't remove notification prefs when updating something else" do
      TagUser.create!(
        user: user,
        tag: tag,
        notification_level: TagUser.notification_levels[:watching],
      )
      CategoryUser.create!(
        user: user,
        category: category,
        notification_level: CategoryUser.notification_levels[:muted],
      )

      updater = UserUpdater.new(acting_user, user)
      updater.update(name: "Steve Dave")

      expect(TagUser.where(user: user).count).to eq(1)
      expect(CategoryUser.where(user: user).count).to eq(1)
    end

    it "updates various fields" do
      updater = UserUpdater.new(acting_user, user)
      date_of_birth = Time.zone.now
      SiteSetting.disable_mailing_list_mode = false

      theme = Fabricate(:theme, user_selectable: true)

      seq = user.user_option.theme_key_seq

      val =
        updater.update(
          bio_raw: "my new bio",
          email_level: UserOption.email_level_types[:always],
          mailing_list_mode: true,
          digest_after_minutes: "45",
          new_topic_duration_minutes: 100,
          auto_track_topics_after_msecs: 101,
          notification_level_when_replying: 3,
          email_in_reply_to: false,
          date_of_birth: date_of_birth,
          theme_ids: [theme.id],
          allow_private_messages: false,
        )

      expect(val).to be_truthy

      user.reload

      expect(user.user_profile.bio_raw).to eq "my new bio"
      expect(user.user_option.email_level).to eq UserOption.email_level_types[:always]
      expect(user.user_option.mailing_list_mode).to eq true
      expect(user.user_option.digest_after_minutes).to eq 45
      expect(user.user_option.new_topic_duration_minutes).to eq 100
      expect(user.user_option.auto_track_topics_after_msecs).to eq 101
      expect(user.user_option.notification_level_when_replying).to eq 3
      expect(user.user_option.email_in_reply_to).to eq false
      expect(user.user_option.theme_ids.first).to eq theme.id
      expect(user.user_option.theme_key_seq).to eq(seq + 1)
      expect(user.user_option.allow_private_messages).to eq(false)
      expect(user.date_of_birth).to eq(date_of_birth.to_date)
    end

    it "allows user to update profile header when the user has required trust level" do
      user = Fabricate(:user, trust_level: 2)
      updater = UserUpdater.new(user, user)
      upload = Fabricate(:upload)
      SiteSetting.min_trust_level_to_allow_profile_background = 2
      val = updater.update(profile_background_upload_url: upload.url)
      expect(val).to be_truthy
      user.reload
      expect(user.profile_background_upload).to eq(upload)
      success = updater.update(profile_background_upload_url: "")
      expect(success).to eq(true)
      user.reload
      expect(user.profile_background_upload).to eq(nil)
    end

    it "allows user to update user card background when the user has required trust level" do
      user = Fabricate(:user, trust_level: 2)
      updater = UserUpdater.new(user, user)
      upload = Fabricate(:upload)
      SiteSetting.min_trust_level_to_allow_user_card_background = 2
      val = updater.update(card_background_upload_url: upload.url)
      expect(val).to be_truthy
      user.reload
      expect(user.card_background_upload).to eq(upload)
      success = updater.update(card_background_upload_url: "")
      expect(success).to eq(true)
      user.reload
      expect(user.card_background_upload).to eq(nil)
    end

    it "disables email_digests when enabling mailing_list_mode" do
      updater = UserUpdater.new(acting_user, user)
      SiteSetting.disable_mailing_list_mode = false

      val = updater.update(mailing_list_mode: true, email_digests: true)
      expect(val).to be_truthy

      user.reload

      expect(user.user_option.email_digests).to eq false
      expect(user.user_option.mailing_list_mode).to eq true
    end

    it "filters theme_ids blank values before updating preferences" do
      user.user_option.update!(theme_ids: [1])
      updater = UserUpdater.new(acting_user, user)

      updater.update(theme_ids: [""])
      user.reload
      expect(user.user_option.theme_ids).to eq([])

      updater.update(theme_ids: [nil])
      user.reload
      expect(user.user_option.theme_ids).to eq([])

      theme = Fabricate(:theme)
      child = Fabricate(:theme, component: true)
      theme.add_relative_theme!(:child, child)
      theme.set_default!

      updater.update(theme_ids: [theme.id.to_s, child.id.to_s, "", nil])
      user.reload
      expect(user.user_option.theme_ids).to eq([theme.id, child.id])
    end

    let(:schedule_attrs) do
      {
        enabled: true,
        day_0_start_time: 30,
        day_0_end_time: 60,
        day_1_start_time: 30,
        day_1_end_time: 60,
        day_2_start_time: 30,
        day_2_end_time: 60,
        day_3_start_time: 30,
        day_3_end_time: 60,
        day_4_start_time: 30,
        day_4_end_time: 60,
        day_5_start_time: 30,
        day_5_end_time: 60,
        day_6_start_time: 30,
        day_6_end_time: 60,
      }
    end

    context "with user_notification_schedule" do
      it "allows users to create their notification schedule when it doesn't exist previously" do
        expect(user.user_notification_schedule).to be_nil
        updater = UserUpdater.new(acting_user, user)

        updater.update(user_notification_schedule: schedule_attrs)
        user.reload
        expect(user.user_notification_schedule.enabled).to eq(true)
        expect(user.user_notification_schedule.day_0_start_time).to eq(30)
        expect(user.user_notification_schedule.day_0_end_time).to eq(60)
        expect(user.user_notification_schedule.day_6_start_time).to eq(30)
        expect(user.user_notification_schedule.day_6_end_time).to eq(60)
      end

      it "allows users to update their notification schedule" do
        UserNotificationSchedule.create({ user: user }.merge(UserNotificationSchedule::DEFAULT))
        updater = UserUpdater.new(acting_user, user)
        updater.update(user_notification_schedule: schedule_attrs)
        user.reload
        expect(user.user_notification_schedule.enabled).to eq(true)
        expect(user.user_notification_schedule.day_0_start_time).to eq(30)
        expect(user.user_notification_schedule.day_0_end_time).to eq(60)
        expect(user.user_notification_schedule.day_6_start_time).to eq(30)
        expect(user.user_notification_schedule.day_6_end_time).to eq(60)
      end

      it "processes the schedule and do_not_disturb_timings are created" do
        updater = UserUpdater.new(acting_user, user)

        expect { updater.update(user_notification_schedule: schedule_attrs) }.to change {
          user.do_not_disturb_timings.count
        }.by(4)
      end

      it "removes do_not_disturb_timings when the schedule is disabled" do
        updater = UserUpdater.new(acting_user, user)
        updater.update(user_notification_schedule: schedule_attrs)
        expect(user.user_notification_schedule.enabled).to eq(true)

        schedule_attrs[:enabled] = false
        updater.update(user_notification_schedule: schedule_attrs)

        expect(user.user_notification_schedule.enabled).to eq(false)
        expect(user.do_not_disturb_timings.count).to eq(0)
      end
    end

    context "when sso overrides bio" do
      it "does not change bio" do
        SiteSetting.discourse_connect_url = "https://www.example.com/sso"
        SiteSetting.enable_discourse_connect = true
        SiteSetting.discourse_connect_overrides_bio = true

        updater = UserUpdater.new(acting_user, user)

        expect(updater.update(bio_raw: "new bio")).to be_truthy

        user.reload
        expect(user.user_profile.bio_raw).not_to eq "new bio"
      end
    end

    context "when sso overrides location" do
      it "does not change location" do
        SiteSetting.discourse_connect_url = "https://www.example.com/sso"
        SiteSetting.enable_discourse_connect = true
        SiteSetting.discourse_connect_overrides_location = true

        updater = UserUpdater.new(acting_user, user)

        expect(updater.update(location: "new location")).to be_truthy

        user.reload
        expect(user.user_profile.location).not_to eq "new location"
      end
    end

    context "when sso overrides website" do
      it "does not change website" do
        SiteSetting.discourse_connect_url = "https://www.example.com/sso"
        SiteSetting.enable_discourse_connect = true
        SiteSetting.discourse_connect_overrides_website = true

        updater = UserUpdater.new(acting_user, user)

        expect(updater.update(website: "https://google.com")).to be_truthy

        user.reload
        expect(user.user_profile.website).not_to eq "https://google.com"
      end
    end

    context "when updating primary group" do
      let(:new_group) { Group.create(name: "new_group") }

      it "updates when setting is enabled" do
        SiteSetting.user_selected_primary_groups = true
        user.groups << new_group
        user.update(primary_group_id: nil)
        UserUpdater.new(acting_user, user).update(primary_group_id: new_group.id)

        user.reload
        expect(user.primary_group_id).to eq new_group.id
      end

      it "does not update when setting is disabled" do
        SiteSetting.user_selected_primary_groups = false
        user.groups << new_group
        user.update(primary_group_id: nil)
        UserUpdater.new(acting_user, user).update(primary_group_id: new_group.id)

        user.reload
        expect(user.primary_group_id).to eq nil
      end

      it "does not update when changing other profile data" do
        SiteSetting.user_selected_primary_groups = true
        user.groups << new_group
        user.update(primary_group_id: new_group.id)
        UserUpdater.new(acting_user, user).update(website: "http://example.com")

        user.reload
        expect(user.primary_group_id).to eq new_group.id
      end

      it "can be removed by the user when setting is enabled" do
        SiteSetting.user_selected_primary_groups = true
        user.groups << new_group
        user.update(primary_group_id: new_group.id)
        UserUpdater.new(acting_user, user).update(primary_group_id: "")

        user.reload
        expect(user.primary_group_id).to eq nil
      end

      it "cannot be removed by the user when setting is disabled" do
        SiteSetting.user_selected_primary_groups = false
        user.groups << new_group
        user.update(primary_group_id: new_group.id)
        UserUpdater.new(acting_user, user).update(primary_group_id: "")

        user.reload
        expect(user.primary_group_id).to eq new_group.id
      end
    end

    context "when updating flair group" do
      let(:group) do
        Fabricate(
          :group,
          name: "Group",
          flair_bg_color: "#111111",
          flair_color: "#999999",
          flair_icon: "icon",
        )
      end

      it "updates when setting is enabled" do
        group.add(user)

        UserUpdater.new(acting_user, user).update(flair_group_id: group.id)
        expect(user.reload.flair_group_id).to eq(group.id)

        UserUpdater.new(acting_user, user).update(flair_group_id: "")
        expect(user.reload.flair_group_id).to eq(nil)
      end
    end

    context "when update fails" do
      it "returns false" do
        user.stubs(save: false)
        updater = UserUpdater.new(acting_user, user)

        expect(updater.update).to be_falsey
      end
    end

    context "with permission to update title" do
      it "allows user to change title" do
        user = Fabricate(:user, title: "Emperor")
        Guardian.any_instance.stubs(:can_grant_title?).with(user, "Minion").returns(true)
        updater = UserUpdater.new(acting_user, user)

        updater.update(title: "Minion")

        expect(user.reload.title).to eq "Minion"
      end
    end

    context "when title is from a badge" do
      fab!(:user) { Fabricate(:user, title: "Emperor") }
      fab!(:badge) { Fabricate(:badge, name: "Minion") }

      context "when badge can be used as a title" do
        before { badge.update(allow_title: true) }

        it "can use as title, sets granted_title_badge_id" do
          BadgeGranter.grant(badge, user)
          updater = UserUpdater.new(user, user)
          updater.update(title: badge.name)
          user.reload
          expect(user.user_profile.granted_title_badge_id).to eq(badge.id)
        end

        it "badge has not been granted, does not change title" do
          badge.update(allow_title: true)
          updater = UserUpdater.new(user, user)
          updater.update(title: badge.name)
          user.reload
          expect(user.title).not_to eq(badge.name)
          expect(user.user_profile.granted_title_badge_id).to be_nil
        end

        it "changing to a title that is not from a badge, unsets granted_title_badge_id" do
          user.update(title: badge.name)
          user.user_profile.update(granted_title_badge_id: badge.id)

          Guardian.any_instance.stubs(:can_grant_title?).with(user, "Dancer").returns(true)

          updater = UserUpdater.new(user, user)
          updater.update(title: "Dancer")
          user.reload
          expect(user.title).to eq("Dancer")
          expect(user.user_profile.granted_title_badge_id).to be_nil
        end
      end

      it "cannot use as title, does not change title" do
        BadgeGranter.grant(badge, user)
        updater = UserUpdater.new(user, user)
        updater.update(title: badge.name)
        user.reload
        expect(user.title).not_to eq(badge.name)
        expect(user.user_profile.granted_title_badge_id).to be_nil
      end
    end

    context "without permission to update title" do
      it "does not allow user to change title" do
        user = Fabricate(:user, title: "Emperor")
        Guardian.any_instance.stubs(:can_grant_title?).with(user, "Minion").returns(false)
        updater = UserUpdater.new(acting_user, user)

        updater.update(title: "Minion")

        expect(user.reload.title).not_to eq "Minion"
      end
    end

    context "when website includes http" do
      it "does not add http before updating" do
        updater = UserUpdater.new(acting_user, user)

        updater.update(website: "http://example.com")

        expect(user.reload.user_profile.website).to eq "http://example.com"
      end
    end

    context "when website does not include http" do
      it "adds http before updating" do
        updater = UserUpdater.new(acting_user, user)

        updater.update(website: "example.com")

        expect(user.reload.user_profile.website).to eq "http://example.com"
      end
    end

    context "when website is invalid" do
      it "returns an error" do
        updater = UserUpdater.new(acting_user, user)

        expect(updater.update(website: "ʔ<")).to eq nil
      end
    end

    context "when custom_fields is empty string" do
      it "update is successful" do
        user.custom_fields = { "import_username" => "my_old_username" }
        user.save
        updater = UserUpdater.new(acting_user, user)

        updater.update(website: "example.com", custom_fields: "")
        expect(user.reload.custom_fields).to eq("import_username" => "my_old_username")
      end
    end

    context "when skip_new_user_tips is edited" do
      it "updates seen_popups too" do
        messages =
          MessageBus.track_publish("/user-tips/#{user.id}") do
            UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: true)
          end

        expect(user.user_option.skip_new_user_tips).to eq(true)
        expect(user.user_option.seen_popups).to eq([-1])
        expect(messages.map(&:data)).to contain_exactly([-1])
      end

      it "does not reset seen_popups" do
        user.user_option.update!(seen_popups: [1, 2, 3])

        UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: false)

        expect(user.user_option.skip_new_user_tips).to eq(false)
        expect(user.user_option.seen_popups).to eq([1, 2, 3])
      end
    end

    context "when seen_popups is edited" do
      it "publishes a message" do
        messages =
          MessageBus.track_publish("/user-tips/#{user.id}") do
            UserUpdater.new(Discourse.system_user, user).update(seen_popups: [1])
          end

        expect(user.user_option.seen_popups).to eq([1])
        expect(messages.map(&:data)).to contain_exactly([1])
      end
    end

    it "logs the action" do
      user = Fabricate(:user, name: "Billy Bob")

      expect do UserUpdater.new(user, user).update(name: "Jim Tom") end.to change {
        UserHistory.count
      }.by(1)

      expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])

      expect do UserUpdater.new(user, user).update(name: "JiM TOm") end.to_not change {
        UserHistory.count
      }

      expect do UserUpdater.new(user, user).update(bio_raw: "foo bar") end.to_not change {
        UserHistory.count
      }

      user_without_name = Fabricate(:user, name: nil)

      expect do
        UserUpdater.new(user_without_name, user_without_name).update(bio_raw: "foo bar")
      end.to_not change { UserHistory.count }

      expect do
        UserUpdater.new(user_without_name, user_without_name).update(name: "Jim Tom")
      end.to change { UserHistory.count }.by(1)

      expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])

      expect do UserUpdater.new(user, user).update(name: "") end.to change { UserHistory.count }.by(
        1,
      )

      expect(UserHistory.last.action).to eq(UserHistory.actions[:change_name])
    end
  end
end


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

class RandomTopicSelector
  BACKFILL_SIZE = 3000
  BACKFILL_LOW_WATER_MARK = 500

  def self.backfill(category = nil)
    exclude = category&.topic_id

    options = {
      per_page: category ? category.num_featured_topics : 3,
      visible: true,
      no_definitions: true,
    }

    options[:except_topic_ids] = [category.topic_id] if exclude

    if category
      options[:category] = category.id
      # NOTE: at the moment this site setting scopes tightly to a category (excluding subcats)
      # this is done so we don't populate a junk cache
      options[:no_subcategories] = true if SiteSetting.limit_suggested_to_category

      # don't leak private categories into the "everything" group
      options[:guardian] = Guardian.new(Discourse.system_user)
    end

    query = TopicQuery.new(nil, options)

    results =
      query
        .latest_results
        .order("RANDOM()")
        .where(closed: false, archived: false)
        .where("topics.created_at > ?", SiteSetting.suggested_topics_max_days_old.days.ago)
        .limit(BACKFILL_SIZE)
        .reorder("RANDOM()")
        .pluck(:id)

    key = cache_key(category)

    if results.present?
      Discourse.redis.multi do |transaction|
        transaction.rpush(key, results)
        transaction.expire(key, 2.days)
      end
    end

    results
  end

  def self.next(count, category = nil)
    key = cache_key(category)

    results = []

    return results if count < 1

    results =
      Discourse.redis.multi do |transaction|
        transaction.lrange(key, 0, count - 1)
        transaction.ltrim(key, count, -1)
      end

    if !results.is_a?(Array) # Redis is in readonly mode
      results = Discourse.redis.lrange(key, 0, count - 1)
    else
      results = results[0]
    end

    results.map!(&:to_i)

    left = count - results.length

    backfilled = false
    if left > 0
      ids = backfill(category)
      backfilled = true
      results += ids[0...count]
      results.uniq!
      results = results[0...count]
    end

    if !backfilled && Discourse.redis.llen(key) < BACKFILL_LOW_WATER_MARK
      Scheduler::Defer.later("backfill") { backfill(category) }
    end

    results
  end

  def self.cache_key(category = nil)
    "random_topic_cache_#{category&.id}"
  end
end

```

Response:
# frozen_string_literal: true

RSpec.describe RandomTopicSelector do
  it "can correctly use cache" do
    key = RandomTopicSelector.cache_key

    Discourse.redis.del key

    4.times { |t| Discourse.redis.rpush key, t }

    expect(RandomTopicSelector.next(0)).to eq([])
    expect(RandomTopicSelector.next(2)).to eq([0, 1])

    Discourse.redis.expects(:multi).returns(Discourse.received_redis_readonly!)
    expect(RandomTopicSelector.next(2)).to eq([2, 3])
    Discourse.redis.unstub(:multi)

    expect(RandomTopicSelector.next(2)).to eq([2, 3])
    expect(RandomTopicSelector.next(2)).to eq([])
  end

  it "can correctly backfill" do
    category = Fabricate(:category, sort_order: "op_likes")
    t1 = Fabricate(:topic, category_id: category.id)
    _t2 = Fabricate(:topic, category_id: category.id, visible: false)
    _t3 = Fabricate(:topic, category_id: category.id, deleted_at: 1.minute.ago)
    t4 = Fabricate(:topic, category_id: category.id)

    expect(RandomTopicSelector.next(5, category).sort).to eq([t1.id, t4.id].sort)
  end
end