# frozen_string_literal: true require 'rails_helper' describe UsernameChanger do before do Jobs.run_immediately! end describe '#change' do let(:user) { Fabricate(:user) } context 'success' do let!(:old_username) { user.username } let(:new_username) { "#{user.username}1234" } it 'should change the username' do 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 end context 'failure' 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 describe 'change 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.to change { UserHistory.count }.by(0) # make sure it does not log a dupe expect do UsernameChanger.change(myself, user.username, myself) end.to change { UserHistory.count }.by(0) # 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 '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) post = create_post(args.merge(topic_id: topic.id)) args.delete(:revisions)&.each do |revision| post.revise(post.user, revision, force_new_version: true) end block.call(post) if block UsernameChanger.change(user, args[:target_username] || 'bar') post.reload end context '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.rstrip).@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.rstrip) 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.rstrip)@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.rstrip)@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(%Q(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(%Q(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 context "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.rstrip)@птица @թռչուն鳥 @թռչուն-鳥 @թռչուն_鳥 @թռչուն٩
HTML end end end context 'quotes' do let(:quoted_post) { create_post(user: user, topic: topic, post_number: 1, raw: "quoted post") } let(:avatar_url) { user.avatar_template.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(<<~HTML.rstrip)Lorem ipsum
dolor sit amet
HTML end context 'simple quote' do let(:raw) do <<~RAW Lorem ipsum [quote="foo, post:1, topic:#{quoted_post.topic.id}"] quoted [/quote] RAW end let(:expected_raw) do <<~RAW.strip Lorem ipsum [quote="bar, post:1, topic:#{quoted_post.topic.id}"] quoted [/quote] RAW end let(:expected_cooked) do <<~HTML.rstripLorem ipsum
HTML end 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 'oneboxes' do let(:quoted_post) { create_post(user: user, topic: topic, post_number: 1, raw: "quoted post") } let(:avatar_url) { user_avatar_url(user) } let(:evil_trout) { Fabricate(:evil_trout) } let(:another_quoted_post) { create_post(user: evil_trout, topic: topic, post_number: 2, raw: "evil post") } def protocol_relative_url(url) url.sub(/^https?:/, '') end def user_avatar_url(u) u.avatar_template.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.rstrip)