docbrown/spec/models/comment_spec.rb
Vaidehi Joshi 484c97fc8d
Limit mentions in posts (#13259)
* Limit mentions in articles

* Bump MAX_USER_MENTION_LIVE_AT to April 7th, 2021 UTC time

* Use stubbed constants in article and comment specs

* Add TODOs around extracting MAX_USER_MENTIONS into constant
2021-04-05 18:19:46 -07:00

547 lines
19 KiB
Ruby

require "rails_helper"
RSpec.describe Comment, type: :model do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
let(:comment) { create(:comment, user: user, commentable: article) }
include_examples "#sync_reactions_count", :article_comment
describe "validations" do
subject { comment }
describe "builtin validations" do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:commentable).optional }
it { is_expected.to have_many(:reactions).dependent(:destroy) }
it { is_expected.to have_many(:mentions).dependent(:destroy) }
it { is_expected.to have_many(:notifications).dependent(:delete_all) }
it { is_expected.to have_many(:notification_subscriptions).dependent(:destroy) }
it { is_expected.to validate_presence_of(:body_markdown) }
it { is_expected.to validate_presence_of(:positive_reactions_count) }
it { is_expected.to validate_presence_of(:public_reactions_count) }
it { is_expected.to validate_presence_of(:reactions_count) }
it { is_expected.to validate_presence_of(:user_id) }
end
it do
# rubocop:disable RSpec/NamedSubject
subject.commentable = article
subject.user = user
expect(subject).to(
validate_uniqueness_of(:body_markdown).scoped_to(:user_id, :ancestry, :commentable_id, :commentable_type),
)
# rubocop:enable RSpec/NamedSubject
end
it { is_expected.to validate_length_of(:body_markdown).is_at_least(1).is_at_most(25_000) }
# rubocop:disable RSpec/NamedSubject
describe "commentable" do
it "is invalid if commentable is an unpublished article" do
subject.commentable = build(:article, published: false)
expect(subject).not_to be_valid
end
it "is valid without a commentable" do
subject.commentable = nil
expect(subject).to be_valid
end
it "checks for commentable_id presence only if commentable_type is present" do
subject.commentable = nil
subject.commentable_type = "Article"
expect(subject).not_to be_valid
end
it "checks for commentable_type inclusion only if commentable_id is present" do
subject.commentable = nil
subject.commentable_id = article.id
expect(subject).not_to be_valid
expect(subject.errors.messages[:commentable_type].first).to match(/not included in the list/)
end
it "is valid with Article commentable type" do
subject.commentable_type = "Article"
expect(subject).to be_valid
end
it "is valid with PodcastEpisode commentable type" do
subject.commentable_type = "PodcastEpisode"
expect(subject).to be_valid
end
it "is not valid with Podcast commentable type" do
subject.commentable_type = "Podcast"
expect(subject).not_to be_valid
end
end
describe "#user_mentions_in_markdown" do
before do
stub_const("Comment::MAX_USER_MENTIONS", 7)
stub_const("Comment::MAX_USER_MENTION_LIVE_AT", 1.day.ago) # Set live_at date to a time in the past
end
it "is valid with any number of mentions if created before MAX_USER_MENTION_LIVE_AT date" do
# Explicitly set created_at date to a time before MAX_USER_MENTION_LIVE_AT
subject.created_at = 3.days.ago
subject.commentable_type = "Article"
subject.body_markdown = "hi @#{user.username}! " * (Comment::MAX_USER_MENTIONS + 1)
expect(subject).to be_valid
end
it "is valid with seven or fewer mentions if created after MAX_USER_MENTION_LIVE_AT date" do
subject.commentable_type = "Article"
subject.body_markdown = "hi @#{user.username}! " * Comment::MAX_USER_MENTIONS
expect(subject).to be_valid
end
it "is invalid with more than seven mentions if created after MAX_USER_MENTION_LIVE_AT date" do
subject.commentable_type = "Article"
subject.body_markdown = "hi @#{user.username}! " * (Comment::MAX_USER_MENTIONS + 1)
expect(subject).not_to be_valid
expect(subject.errors[:base])
.to include("You cannot mention more than #{Comment::MAX_USER_MENTIONS} users in a comment!")
end
end
# rubocop:enable RSpec/NamedSubject
describe "#after_commit" do
it "on update enqueues job to index comment to elasticsearch" do
sidekiq_assert_enqueued_with(job: Search::IndexWorker, args: [described_class.to_s, comment.id]) do
comment.save
end
end
it "on destroy enqueues job to delete comment from elasticsearch" do
comment = create(:comment)
sidekiq_assert_enqueued_with(job: Search::RemoveFromIndexWorker,
args: [described_class::SEARCH_CLASS.to_s, comment.search_id]) do
comment.destroy
end
end
end
describe "#search_id" do
it "returns comment_ID" do
expect(comment.search_id).to eq("comment_#{comment.id}")
end
end
describe "#processed_html" do
let(:comment) { build(:comment, user: user, commentable: article) }
it "converts body_markdown to proper processed_html" do
comment.body_markdown = "# hello\n\nhy hey hey"
comment.validate!
expect(comment.processed_html.include?("<h1>")).to be(true)
end
it "adds rel=nofollow to links" do
comment.body_markdown = "this is a comment with a link: http://dev.to"
comment.validate!
expect(comment.processed_html.include?('rel="nofollow"')).to be(true)
end
it "adds a mention url if user is mentioned like @mention" do
comment.body_markdown = "Hello @#{user.username}, you are cool."
comment.validate!
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
expect(comment.processed_html.include?("href")).to be(true)
expect(comment.processed_html.include?("Hello <a")).to be(true)
end
it "not double wrap an already-linked mention" do
comment.body_markdown = "Hello <a href='/#{user.username}'>@#{user.username}</a>, you are cool."
comment.validate!
expect(comment.processed_html.scan(/href/).count).to eq(1)
end
it "does not wrap email mention with username" do
comment.body_markdown = "Hello hello@#{user.username}.com, you are cool."
comment.validate!
expect(comment.processed_html.include?("/#{user.username}")).to be(false)
end
it "only mentions users who are actual users" do
comment.body_markdown = "Hello @hooper, you are cool."
comment.validate!
expect(comment.processed_html.include?("/hooper")).to be(false)
end
it "mentions people if it is the first word" do
comment.body_markdown = "@#{user.username}, you are cool."
comment.validate!
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
end
it "does case insentive mention recognition" do
comment.body_markdown = "Hello @#{user.username.titleize}, you are cool."
comment.validate!
expect(comment.processed_html.include?("/#{user.username}")).to be(true)
expect(comment.processed_html.include?("href")).to be(true)
expect(comment.processed_html.include?("Hello <a")).to be(true)
end
it "shortens long urls" do
comment.body_markdown = "Hello https://longurl.com/#{'x' * 100}?#{'y' * 100}"
comment.validate!
expect(comment.processed_html.include?("...</a>")).to be(true)
expect(comment.processed_html.size < 450).to be(true)
end
# rubocop:disable RSpec/ExampleLength
it "adds timestamp url if commentable has video and timestamp", :aggregate_failures do
article.video = "https://example.com"
comment.body_markdown = "I like the part at 4:30"
comment.validate!
expect(comment.processed_html.include?(">4:30</a>")).to be(true)
comment.body_markdown = "I like the part at 4:30 and 5:50"
comment.validate!
expect(comment.processed_html.include?(">5:50</a>")).to eq(true)
comment.body_markdown = "I like the part at 5:30 and :55"
comment.validate!
expect(comment.processed_html.include?(">:55</a>")).to eq(true)
comment.body_markdown = "I like the part at 52:30"
comment.validate!
expect(comment.processed_html.include?(">52:30</a>")).to eq(true)
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
comment.validate!
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(true)
expect(comment.processed_html.include?(">1:20</a>")).to eq(true)
end
# rubocop:enable RSpec/ExampleLength
it "does not add timestamp if commentable does not have video" do
article.video = nil
comment.body_markdown = "I like the part at 1:52:30 and 1:20"
comment.validate!
expect(comment.processed_html.include?(">1:52:30</a>")).to eq(false)
end
it "does not add DOCTYPE and html body to processed html" do
comment.body_markdown = "Hello https://longurl.com/#{'x' * 100}?#{'y' * 100}"
comment.validate!
expect(comment.processed_html).not_to include("<!DOCTYPE")
expect(comment.processed_html).not_to include("<html><body>")
end
end
end
describe "#id_code_generated" do
it "gets proper generated ID code" do
expect(described_class.new(id: 1000).id_code_generated).to eq("1cc")
end
end
describe "#readable_publish_date" do
it "does not show year in readable time if not current year" do
expect(comment.readable_publish_date).to eq(comment.created_at.strftime("%b %e"))
end
it "shows year in readable time if not current year" do
comment.created_at = 1.year.ago
last_year = 1.year.ago.year % 100
expect(comment.readable_publish_date.include?("'#{last_year}")).to eq(true)
end
end
describe "#path" do
it "returns the properly formed path" do
expect(comment.path).to eq("/#{comment.user.username}/comment/#{comment.id_code_generated}")
end
end
describe "#parent_or_root_article" do
it "returns root article if no parent comment" do
expect(comment.parent_or_root_article).to eq(comment.commentable)
end
it "returns root parent comment if exists" do
child_comment = build(:comment, parent: comment)
expect(child_comment.parent_or_root_article).to eq(comment)
end
end
describe "#parent_user" do
it "returns the root article's user if no parent comment" do
expect(comment.parent_user).to eq(user)
end
it "returns the root parent comment's user if root parent comment exists" do
child_comment_user = build(:user)
child_comment = build(:comment, parent: comment, user: child_comment_user)
expect(child_comment.parent_user).not_to eq(child_comment_user)
expect(child_comment.parent_user).to eq(comment.user)
end
end
describe "#title" do
it "is no more than 80 characters" do
expect(comment.title.length).to be <= 80
end
it "is allows title of greater length if passed" do
expect(comment.title(5).length).to eq(5)
end
it "gets content from body_markdown" do
comment.body_markdown = "Migas fingerstache pbr&b tofu."
comment.validate!
expect(comment.title).to eq("Migas fingerstache pbr&b tofu.")
end
it "is converted to deleted if the comment is deleted" do
comment.deleted = true
expect(comment.title).to eq("[deleted]")
end
it "does not contain the wrong encoding" do
comment.body_markdown = "It's the best post ever. It's so great."
comment.validate!
expect(comment.title).not_to include("&#39;")
end
end
describe "#custom_css" do
it "returns nothing when no liquid tag was used" do
expect(comment.custom_css).to be_blank
end
it "returns proper liquid tag classes if used" do
text = "{% devcomment #{comment.id_code_generated} %}"
comment.body_markdown = text
expect(comment.custom_css).to be_present
end
end
describe ".tree_for" do
let!(:other_comment) { create(:comment, commentable: article, user: user) }
let!(:child_comment) { create(:comment, commentable: article, parent: comment, user: user) }
before { comment.update_column(:score, 1) }
it "returns a full tree" do
comments = described_class.tree_for(article)
expect(comments).to eq(comment => { child_comment => {} }, other_comment => {})
end
it "returns part of the tree" do
comments = described_class.tree_for(article, 1)
expect(comments).to eq(comment => { child_comment => {} })
end
end
context "when callbacks are triggered after create" do
let(:comment) { build(:comment, user: user, commentable: article) }
it "creates an id code" do
comment.save
expect(comment.reload.id_code).to eq(comment.id.to_s(26))
end
it "enqueue a worker to create the first reaction" do
expect do
comment.save
end.to change(Comments::CreateFirstReactionWorker.jobs, :size).by(1)
end
it "enqueues a worker to calculate comment score" do
expect do
comment.save
end.to change(Comments::CalculateScoreWorker.jobs, :size).by(1)
end
it "enqueues a worker to send email" do
comment.save!
child_comment_user = create(:user)
child_comment = build(:comment, parent: comment, user: child_comment_user, commentable: article)
expect do
child_comment.save!
end.to change(Comments::SendEmailNotificationWorker.jobs, :size).by(1)
end
it "enqueues a worker to bust comment cache" do
expect do
comment.save
end.to change(Comments::BustCacheWorker.jobs, :size).by(1)
end
it "touches user updated_at" do
user.updated_at = 1.month.ago
user.save
expect { comment.save }.to change(user, :updated_at)
end
it "touches user last_comment_at" do
user.last_comment_at = 1.month.ago
user.save
expect { comment.save }.to change(user, :last_comment_at)
end
describe "slack messages" do
let!(:user) { create(:user) }
before do
article
comment
# making sure there are no other enqueued jobs from other tests
sidekiq_perform_enqueued_jobs(only: Slack::Messengers::Worker)
end
it "queues a slack message when a warned user leaves a comment" do
user.add_role(:warned)
sidekiq_assert_enqueued_jobs(1, only: Slack::Messengers::Worker) do
create(:comment, user: user, commentable: article)
end
end
it "does not send notification if a regular user leaves a comment" do
sidekiq_assert_no_enqueued_jobs(only: Slack::Messengers::Worker) do
create(:comment, commentable: article, user: user)
end
end
end
end
describe "spam" do
before do
allow(SiteConfig).to receive(:mascot_user_id).and_return(user.id)
allow(SiteConfig).to receive(:spam_trigger_terms).and_return(["yahoomagoo gogo", "anothertestterm"])
end
it "creates vomit reaction if possible spam" do
comment.body_markdown = "This post is about Yahoomagoo gogo"
comment.save
expect(Reaction.last.category).to eq("vomit")
expect(Reaction.last.user_id).to eq(user.id)
end
it "does no suspend user if only single vomit" do
comment.body_markdown = "This post is about Yahoomagoo gogo"
comment.save
expect(comment.user.banned).to be false
end
it "suspends user with 3 comment vomits" do
comment.body_markdown = "This post is about Yahoomagoo gogo"
second_comment = create(:comment, user: comment.user, body_markdown: "This post is about Yahoomagoo gogo")
third_comment = create(:comment, user: comment.user, body_markdown: "This post is about Yahoomagoo gogo")
comment.save
second_comment.save
third_comment.save
expect(comment.user.banned).to be true
expect(Note.last.reason).to eq "automatic_suspend"
end
it "does not create vomit reaction if user is established in this context" do
user.update_column(:registered_at, 10.days.ago)
comment.body_markdown = "This post is about Yahoomagoo gogo"
comment.save
expect(Reaction.last).to be nil
end
it "does not create vomit reaction if does not have matching title" do
comment.save
expect(Reaction.last).to be nil
end
end
context "when callbacks are triggered before save" do
it "generates character count before saving" do
comment.save
expect(comment.markdown_character_count).to eq(comment.body_markdown.size)
end
end
context "when callbacks are triggered after save" do
it "updates user last comment date" do
comment = build(:comment, commentable: article, user: user)
expect { comment.save }.to change(user, :last_comment_at)
end
end
context "when callbacks are triggered after update" do
it "deletes the comment's notifications when deleted is set to true" do
create(:notification, notifiable: comment, user: user)
sidekiq_perform_enqueued_jobs do
comment.update(deleted: true)
end
expect(comment.notifications).to be_empty
end
it "deletes the comment's notifications when hidden_by_commentable_user is set to true" do
create(:notification, notifiable: comment, user: user)
sidekiq_perform_enqueued_jobs do
comment.update(hidden_by_commentable_user: true)
end
expect(comment.notifications).to be_empty
end
it "updates the notifications of the descendants with [deleted]" do
comment = create(:comment, commentable: article)
child_comment = create(:comment, parent: comment, commentable: article, user: user)
create(:notification, notifiable: child_comment, user: user)
sidekiq_perform_enqueued_jobs do
comment.update(deleted: true)
end
notification = child_comment.notifications.first
expect(notification.json_data["comment"]["ancestors"][0]["title"]).to eq("[deleted]")
end
end
context "when callbacks are triggered after destroy" do
let!(:comment) { create(:comment, user: user, commentable: article) }
it "updates user's last_comment_at" do
comment = create(:comment, user: user)
expect { comment.destroy }.to change(user, :last_comment_at)
end
it "busts the comment cache" do
sidekiq_assert_enqueued_with(job: Comments::BustCacheWorker, args: [comment.id]) do
comment.destroy
end
end
end
describe "#root_exists?" do
let(:root_comment) { create(:comment) }
let(:comment) { create(:comment, ancestry: root_comment.id) }
it "returns true if root is present" do
expect(comment.root_exists?).to eq(true)
end
it "returns false if root has been deleted" do
root_comment.destroy
expect(comment.reload.root_exists?).to eq(false)
end
end
end