# encoding: utf-8 # frozen_string_literal: true RSpec.describe Topic do let(:now) { Time.zone.local(2013, 11, 20, 8, 0) } fab!(:user) { Fabricate(:user) } fab!(:user1) { Fabricate(:user) } fab!(:whisperers_group) { Fabricate(:group) } fab!(:user2) { Fabricate(:user, groups: [whisperers_group]) } fab!(:moderator) { Fabricate(:moderator) } fab!(:coding_horror) { Fabricate(:coding_horror) } fab!(:evil_trout) { Fabricate(:evil_trout) } fab!(:admin) { Fabricate(:admin) } fab!(:group) { Fabricate(:group) } fab!(:trust_level_2) { Fabricate(:user, trust_level: SiteSetting.min_trust_level_to_allow_invite) } describe 'Validations' do let(:topic) { Fabricate.build(:topic) } describe "#featured_link" do describe 'when featured_link contains more than a URL' do it 'should not be valid' do topic.featured_link = 'http://meta.discourse.org TEST' expect(topic).to_not be_valid end end describe 'when featured_link is a valid URL' do it 'should be valid' do topic.featured_link = 'http://meta.discourse.org' expect(topic).to be_valid end end end describe "#external_id" do describe 'when external_id is too long' do it 'should not be valid' do topic.external_id = 'a' * (Topic::EXTERNAL_ID_MAX_LENGTH + 1) expect(topic).to_not be_valid end end describe 'when external_id has invalid characters' do it 'should not be valid' do topic.external_id = 'a*&^!@()#' expect(topic).to_not be_valid end end describe 'when external_id is an empty string' do it 'should not be valid' do topic.external_id = '' expect(topic).to_not be_valid end end describe 'when external_id has already been used' do it 'should not be valid' do topic2 = Fabricate(:topic, external_id: 'asdf') topic.external_id = 'asdf' expect(topic).to_not be_valid end end describe 'when external_id is nil' do it 'should be valid' do topic.external_id = nil expect(topic).to be_valid end end describe 'when external_id is valid' do it 'should be valid' do topic.external_id = 'abc_123-ZXY' expect(topic).to be_valid end end end describe "#title" do it { is_expected.to validate_presence_of :title } describe 'censored words' do after do Discourse.redis.flushdb end describe 'when title contains censored words' do after do WordWatcher.clear_cache! end it 'should not be valid' do ['pineapple', 'pen'].each { |w| Fabricate(:watched_word, word: w, action: WatchedWord.actions[:censor]) } topic.title = 'pen PinEapple apple pen is a complete sentence' expect(topic).to_not be_valid expect(topic.errors.full_messages.first).to include(I18n.t( 'errors.messages.contains_censored_words', censored_words: 'pen, pineapple' )) end end describe 'titles with censored words not on boundaries' do it "should be valid" do Fabricate(:watched_word, word: 'apple', action: WatchedWord.actions[:censor]) topic.title = "Pineapples are great fruit! Applebee's is a great restaurant" expect(topic).to be_valid end end describe 'when title does not contain censored words' do it 'should be valid' do topic.title = 'The cake is a lie' expect(topic).to be_valid end end describe 'escape special characters in censored words' do before do ['co(onut', 'coconut', 'a**le'].each do |w| Fabricate(:watched_word, word: w, action: WatchedWord.actions[:censor]) end end it 'should not be valid' do topic.title = "I have a co(onut a**le" expect(topic.valid?).to eq(false) expect(topic.errors.full_messages.first).to include(I18n.t( 'errors.messages.contains_censored_words', censored_words: 'co(onut, a**le' )) end end end describe 'blocked words' do describe 'when title contains watched words' do after do WordWatcher.clear_cache! end it 'should not be valid' do Fabricate(:watched_word, word: 'pineapple', action: WatchedWord.actions[:block]) topic.title = 'pen PinEapple apple pen is a complete sentence' expect(topic).to_not be_valid expect(topic.errors.full_messages.first).to include(I18n.t( 'contains_blocked_word', word: 'PinEapple' )) end end end end end it { is_expected.to rate_limit } describe '#visible_post_types' do let(:types) { Post.types } before do SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}|#{whisperers_group.id}" end it "returns the appropriate types for anonymous users" do post_types = Topic.visible_post_types expect(post_types).to include(types[:regular]) expect(post_types).to include(types[:moderator_action]) expect(post_types).to include(types[:small_action]) expect(post_types).to_not include(types[:whisper]) end it "returns the appropriate types for regular users" do post_types = Topic.visible_post_types(Fabricate.build(:user)) expect(post_types).to include(types[:regular]) expect(post_types).to include(types[:moderator_action]) expect(post_types).to include(types[:small_action]) expect(post_types).to_not include(types[:whisper]) end it "returns the appropriate types for staff users" do post_types = Topic.visible_post_types(moderator) expect(post_types).to include(types[:regular]) expect(post_types).to include(types[:moderator_action]) expect(post_types).to include(types[:small_action]) expect(post_types).to include(types[:whisper]) end it "returns the appropriate types for whisperer users" do post_types = Topic.visible_post_types(user2) expect(post_types).to include(types[:regular]) expect(post_types).to include(types[:moderator_action]) expect(post_types).to include(types[:small_action]) expect(post_types).to include(types[:whisper]) end end describe 'slug' do context 'with encoded generator' do before { SiteSetting.slug_generation_method = 'encoded' } context 'with ascii letters' do let!(:title) { "hello world topic" } let!(:slug) { "hello-world-topic" } let!(:topic) { Fabricate.build(:topic, title: title) } it "returns a Slug for a title" do expect(topic.title).to eq(title) expect(topic.slug).to eq(slug) end end context 'for cjk characters' do let!(:title) { "熱帶風暴畫眉" } let!(:topic) { Fabricate.build(:topic, title: title) } it "returns encoded Slug for a title" do expect(topic.title).to eq(title) expect(topic.slug).to eq('%E7%86%B1%E5%B8%B6%E9%A2%A8%E6%9A%B4%E7%95%AB%E7%9C%89') end end context 'for numbers' do let!(:title) { "123456789" } let!(:slug) { "topic" } let!(:topic) { Fabricate.build(:topic, title: title) } it 'generates default slug' do Slug.expects(:for).with(title).returns("topic") expect(Fabricate.build(:topic, title: title).slug).to eq("topic") end end end context 'with none generator' do let!(:title) { "熱帶風暴畫眉" } let!(:slug) { "topic" } let!(:topic) { Fabricate.build(:topic, title: title) } before { SiteSetting.slug_generation_method = 'none' } it "returns a Slug for a title" do Slug.expects(:for).with(title).returns('topic') expect(Fabricate.build(:topic, title: title).slug).to eq(slug) end end describe '#ascii_generator' do before { SiteSetting.slug_generation_method = 'ascii' } context 'with ascii letters' do let!(:title) { "hello world topic" } let!(:slug) { "hello-world-topic" } let!(:topic) { Fabricate.build(:topic, title: title) } it "returns a Slug for a title" do Slug.expects(:for).with(title).returns(slug) expect(Fabricate.build(:topic, title: title).slug).to eq(slug) end end context 'for cjk characters' do let!(:title) { "熱帶風暴畫眉" } let!(:slug) { 'topic' } let!(:topic) { Fabricate.build(:topic, title: title) } it "returns 'topic' when the slug is empty (say, non-latin characters)" do Slug.expects(:for).with(title).returns("topic") expect(Fabricate.build(:topic, title: title).slug).to eq("topic") end end end describe 'slug computed hooks' do before do invert_slug = ->(topic, slug, title) { slug.reverse } Topic.slug_computed_callbacks << invert_slug end let!(:title) { "hello test topic" } let!(:slug) { "hello-test-topic".reverse } let!(:other_title) { "other title" } let!(:other_slug) { "other-title".reverse } let!(:topic) { Fabricate.build(:topic, title: title) } it "returns a reversed slug for a title" do expect(topic.title).to eq(title) expect(topic.slug).to eq(slug) end it "returns a reversed slug after the title is changed" do expect(topic.title).to eq(title) expect(topic.slug).to eq(slug) topic.title = other_title expect(topic.title).to eq(other_title) expect(topic.slug).to eq(other_slug) end after do Topic.slug_computed_callbacks.clear end end end describe "updating a title to be shorter" do let!(:topic) { Fabricate(:topic) } it "doesn't update it to be shorter due to cleaning using TextCleaner" do topic.title = 'unread glitch' expect(topic.save).to eq(false) end end describe 'private message title' do before do SiteSetting.min_topic_title_length = 15 SiteSetting.min_personal_message_title_length = 3 end it 'allows shorter titles' do pm = Fabricate.build(:private_message_topic, title: 'a' * SiteSetting.min_personal_message_title_length) expect(pm).to be_valid end it 'but not too short' do pm = Fabricate.build(:private_message_topic, title: 'a') expect(pm).to_not be_valid end end describe 'admin topic title' do it 'allows really short titles' do pm = Fabricate.build(:private_message_topic, user: admin, title: 'a') expect(pm).to be_valid end it 'but not blank' do pm = Fabricate.build(:private_message_topic, title: '') expect(pm).to_not be_valid end end describe 'topic title uniqueness' do fab!(:category1) { Fabricate(:category) } fab!(:category2) { Fabricate(:category) } fab!(:topic) { Fabricate(:topic, category: category1) } let(:new_topic) { Fabricate.build(:topic, title: topic.title, category: category1) } let(:new_topic_different_cat) { Fabricate.build(:topic, title: topic.title, category: category2) } context "when duplicates aren't allowed" do before do SiteSetting.allow_duplicate_topic_titles = false SiteSetting.allow_duplicate_topic_titles_category = false end it "won't allow another topic to be created with the same name" do expect(new_topic).not_to be_valid end it "won't even allow another topic to be created with the same name but different category" do expect(new_topic_different_cat).not_to be_valid end it "won't allow another topic with an upper case title to be created" do new_topic.title = new_topic.title.upcase expect(new_topic).not_to be_valid end it "allows it when the topic is deleted" do topic.destroy expect(new_topic).to be_valid end it "allows a private message to be created with the same topic" do new_topic.archetype = Archetype.private_message expect(new_topic).to be_valid end end context "when duplicates are allowed" do before do SiteSetting.allow_duplicate_topic_titles = true SiteSetting.allow_duplicate_topic_titles_category = false end it "will allow another topic to be created with the same name" do expect(new_topic).to be_valid end end context "when duplicates are allowed if the category is different" do before do SiteSetting.allow_duplicate_topic_titles = false SiteSetting.allow_duplicate_topic_titles_category = true end it "will allow another topic to be created with the same name but different category" do expect(new_topic_different_cat).to be_valid end it "won't allow another topic to be created with the same name in same category" do expect(new_topic).not_to be_valid end it "other errors will not be cleared" do SiteSetting.min_topic_title_length = 5 topic.update!(title: "more than 5 characters but less than 134") SiteSetting.min_topic_title_length = 134 new_topic_different_cat.title = "more than 5 characters but less than 134" expect(new_topic_different_cat).not_to be_valid expect(new_topic_different_cat.errors[:title]).to include(I18n.t("errors.messages.too_short", count: 134)) end end end describe 'html in title' do def build_topic_with_title(title) build(:topic, title: title).tap { |t| t.valid? } end let(:topic_bold) { build_topic_with_title("Topic with bold text in its title") } let(:topic_image) { build_topic_with_title("Topic with image in its title") } let(:topic_script) { build_topic_with_title("Topic with script in its title") } let(:topic_emoji) { build_topic_with_title("I 💖 candy alot") } let(:topic_modifier_emoji) { build_topic_with_title("I 👨‍🌾 candy alot") } let(:topic_shortcut_emoji) { build_topic_with_title("I love candy :)") } let(:topic_inline_emoji) { build_topic_with_title("Hello😊World") } it "escapes script contents" do expect(topic_script.fancy_title).to eq("Topic with <script>alert(‘title’)</script> script in its title") end it "expands emojis" do expect(topic_emoji.fancy_title).to eq("I :sparkling_heart: candy alot") end it "keeps combined emojis" do expect(topic_modifier_emoji.fancy_title).to eq("I :man_farmer: candy alot") end it "escapes bold contents" do expect(topic_bold.fancy_title).to eq("Topic with <b>bold</b> text in its title") end it "escapes image contents" do expect(topic_image.fancy_title).to eq("Topic with <img src=‘something’> image in its title") end it "always escapes title" do topic_script.title = topic_script.title + "x" * Topic.max_fancy_title_length expect(topic_script.fancy_title).to eq(ERB::Util.html_escape(topic_script.title)) # not really needed, but just in case expect(topic_script.fancy_title).not_to include("