# 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(
Hello @bar
)) post.rebake! expect(post.cooked).to eq(%Q(Hello @bar
)) 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(There’s no difference between @bar and @bar
), ) 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.@bar -@bar %@bar _@bar ,@bar ;@bar @@bar
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(), ) 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 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@bar. @bar, @bar: @bar; @bar_ @bar-
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(@foo_ @bar
), ) 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(@bar a@foo 2@foo
), ) 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(), ) 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@bar @foobar @foo-bar @foo_bar @foo1
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(Hello @bar
)) 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(Hello @bar!!
)) 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(Hello @bar
), ) expect(post.revisions[0].modifications["cooked"][1]).to eq("Hello Foo
") 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("Hello Foo
") expect(post.revisions[2].modifications["cooked"][1]).to eq( %Q(Hello @bar!
), ) 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(Hello @bar!
), ) expect(post.revisions[3].modifications["cooked"][1]).to eq( %Q(Hello @bar!!
), ) 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(Hello @bar
), ) end it "works when users are mentioned with HTML" do post = create_post_and_change_username( raw: '@foo and @someuser', ) expect(post.raw).to eq('@bar and @someuser') expect(post.cooked).to match_html( '', ) 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(Hello @bar
)) 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(Hello @bar!!
)) 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(Hello @bar
), ) expect(post.revisions[0].modifications["cooked"][1]).to eq("Hello Foo
") 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("Hello Foo
") expect(post.revisions[2].modifications["cooked"][1]).to eq( %Q(Hello @bar!
), ) 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(Hello @bar!
), ) expect(post.revisions[3].modifications["cooked"][1]).to eq( %Q(Hello @bar!!
), ) 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(Hello @птица
), ) 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(Hello @птица 鳥@թռչուն 2@թռչուն ٩@թռչուն
), ) 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@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩
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}", "40") } 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 <<~HTMLLorem ipsum
dolor sit amet
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 }Lorem ipsum
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}", "40") 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 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 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