otwarchive-symphonyarchive/spec/models/comment_spec.rb

772 lines
28 KiB
Ruby
Raw Normal View History

2026-03-11 22:22:11 +00:00
# frozen_string_literal: true
require "spec_helper"
describe Comment do
include ActiveJob::TestHelper
def queue_adapter_for_test
ActiveJob::QueueAdapters::TestAdapter.new
end
describe "validations" do
context "with a forbidden guest name" do
subject { build(:comment, email: Faker::Internet.email) }
let(:forbidden_name) { Faker::Lorem.characters(number: 8) }
before do
allow(ArchiveConfig).to receive(:FORBIDDEN_USERNAMES).and_return([forbidden_name])
end
it { is_expected.not_to allow_values(forbidden_name, forbidden_name.swapcase).for(:name) }
it "does not prevent saving when the name is unchanged" do
subject.name = forbidden_name
subject.save!(validate: false)
expect(subject.save).to be_truthy
end
it "does not prevent deletion" do
subject.name = forbidden_name
subject.save!(validate: false)
subject.destroy
expect { subject.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples "disallows editing the comment if it's changed significantly" do
it "prevents editing the comment if it's changed significantly" do
subject.edited_at = Time.current
subject.comment_content = "Spam content" * 12
expect(subject.save).to be_falsey
expect(subject.errors[:base]).to include("This comment looks like spam to our system, sorry! Please try again.")
end
it "allows editing the comment if it's not changed significantly" do
subject.edited_at = Time.current
subject.comment_content += "a"
expect(subject.save).to be_truthy
end
it "allows modifying the comment besides content" do
subject.hidden_by_admin = true
expect(subject.save).to be_truthy
end
end
shared_examples "always allows changing the comment" do
it "allows editing the comment if it's changed significantly" do
subject.edited_at = Time.current
subject.comment_content = "Spam content" * 12
expect(subject.save).to be_truthy
end
it "allows editing the comment if it's not changed significantly" do
subject.edited_at = Time.current
subject.comment_content += "a"
expect(subject.save).to be_truthy
end
end
context "when any comment is considered spam" do
subject { build(:comment, pseud: user.default_pseud) }
let(:admin_setting) { AdminSetting.first || AdminSetting.create }
before do
subject.save!
allow_any_instance_of(Comment).to receive(:spam?).and_return(true)
end
context "when account_age_threshold_for_comment_spam_check is set" do
before do
admin_setting.update_attribute(:account_age_threshold_for_comment_spam_check, 10)
end
context "for a new user" do
let(:user) { create(:user, created_at: Time.current) }
it_behaves_like "disallows editing the comment if it's changed significantly"
context "on the commenters own work" do
subject { build(:comment, pseud: user.default_pseud, commentable: work) }
let(:work) { create(:work, authors: [user.default_pseud]) }
it_behaves_like "always allows changing the comment"
context "comment is a reply" do
subject { build(:comment, pseud: user.default_pseud, commentable: comment) }
let(:comment) { build(:comment, commentable: work) }
it_behaves_like "always allows changing the comment"
end
end
context "on a tag" do
subject { build(:comment, :on_tag, pseud: user.default_pseud) }
it_behaves_like "always allows changing the comment"
end
context "on an admin post" do
subject { build(:comment, :on_admin_post, pseud: user.default_pseud) }
it_behaves_like "disallows editing the comment if it's changed significantly"
end
end
context "for an old user" do
let(:user) { create(:user, created_at: 12.days.ago) }
it_behaves_like "always allows changing the comment"
context "on the commenters own work" do
subject { build(:comment, pseud: user.default_pseud, commentable: work) }
let(:work) { create(:work, authors: [user.default_pseud]) }
it_behaves_like "always allows changing the comment"
end
context "on a tag" do
subject { build(:comment, :on_tag, pseud: user.default_pseud) }
it_behaves_like "always allows changing the comment"
end
context "on an admin post" do
subject { build(:comment, :on_admin_post, pseud: user.default_pseud) }
it_behaves_like "always allows changing the comment"
end
context "when they change their email address" do
before do
user.update!(confirmed_at: Time.current)
end
subject { build(:comment, :on_admin_post, pseud: user.default_pseud) }
it_behaves_like "always allows changing the comment"
end
end
end
context "when account_age_threshold_for_comment_spam_check is unset" do
before do
admin_setting.update_attribute(:account_age_threshold_for_comment_spam_check, 0)
end
context "for a new user" do
let(:user) { create(:user, created_at: Time.current) }
it_behaves_like "always allows changing the comment"
context "on the commenters own work" do
subject { build(:comment, pseud: user.default_pseud, commentable: work) }
let(:work) { create(:work, authors: [user.default_pseud]) }
it_behaves_like "always allows changing the comment"
end
context "on an admin post" do
subject { build(:comment, :on_admin_post, pseud: user.default_pseud) }
it_behaves_like "always allows changing the comment"
end
end
end
end
context "when submitting comment to Akismet" do
subject { create(:comment) }
it "has user_role \"user\"" do
expect(subject.akismet_attributes[:user_role]).to eq("user")
end
it "has comment_author as the user's username" do
expect(subject.akismet_attributes[:comment_author]).to eq(subject.pseud.user.login)
end
it "has comment_author_email as the user's email" do
expect(subject.akismet_attributes[:comment_author_email]).to eq(subject.pseud.user.email)
end
context "when the comment is being created" do
let(:new_comment) do
Comment.new(commentable: subject,
pseud: create(:user).default_pseud,
comment_content: "Hmm.")
end
it "does not set recheck_reason" do
expect(new_comment.akismet_attributes).not_to have_key(:recheck_reason)
end
end
context "when the comment is being edited" do
it "sets recheck_reason to 'edit'" do
subject.edited_at = Time.current
subject.comment_content += " updated"
expect(subject.akismet_attributes[:recheck_reason]).to eq("edit")
end
end
context "when the comment is from a guest" do
subject { create(:comment, :by_guest) }
it "has user_role \"guest\"" do
expect(subject.akismet_attributes[:user_role]).to eq("guest")
end
it "has comment_author as the commenter's name" do
expect(subject.akismet_attributes[:comment_author]).to eq(subject.name)
end
it "has comment_author_email as the commenter's email" do
expect(subject.akismet_attributes[:comment_author_email]).to eq(subject.email)
end
end
context "when the commentable is a chapter" do
it "has comment_type \"fanwork-comment\"" do
expect(subject.akismet_attributes[:comment_type]).to eq("fanwork-comment")
end
end
context "when the commentable is an admin post" do
subject { create(:comment, :on_admin_post) }
it "has comment_type \"comment\"" do
expect(subject.akismet_attributes[:comment_type]).to eq("comment")
end
end
context "when the commentable is a comment" do
context "when the comment is on a chapter" do
subject { create(:comment, commentable: create(:comment)) }
it "has comment_type \"fanwork-comment\"" do
expect(subject.akismet_attributes[:comment_type]).to eq("fanwork-comment")
end
end
context "when the comment is on an admin post" do
subject { create(:comment, commentable: create(:comment, :on_admin_post)) }
it "has comment_type \"comment\"" do
expect(subject.akismet_attributes[:comment_type]).to eq("comment")
end
end
end
end
end
context "with an existing comment from the same user" do
let(:first_comment) { create(:comment) }
let(:second_comment) do
attributes = %w[pseud_id commentable_id commentable_type comment_content name email]
Comment.new(first_comment.attributes.slice(*attributes))
end
it "should be invalid if exactly duplicated" do
expect(second_comment.valid?).to be_falsy
expect(second_comment.errors.attribute_names).to include(:comment_content)
expect(second_comment.errors.full_messages.first).to include("You've already")
end
it "should not be invalid if in the process of being deleted" do
second_comment.is_deleted = true
expect(second_comment.valid?).to be_truthy
end
end
context "with blocking" do
let(:blocked) { create(:user) }
let(:comment) do
Comment.new(commentable: commentable,
pseud: blocked.default_pseud,
comment_content: "Hmm.")
end
before { Block.create(blocker: blocker, blocked: blocked) }
shared_examples "creating and editing comments is allowed" do
describe "save" do
it "allows new comments" do
expect(comment.save).to be_truthy
expect(comment.errors.full_messages).to be_blank
end
end
describe "update" do
before { comment.save(validate: false) }
it "changes the comment" do
comment.update!(comment_content: "Why did you block me?")
expect(comment.errors.full_messages).to be_blank
expect(comment.reload.comment_content).to eq("Why did you block me?")
end
end
end
shared_examples "creating and editing comments is not allowed" do |message:|
describe "save" do
it "doesn't allow new comments" do
expect(comment.save).to be_falsey
expect(comment.errors.full_messages).to include(message)
end
end
describe "update" do
before { comment.save(validate: false) }
it "doesn't change the comment" do
expect { comment.update!(comment_content: "Why did you block me?") }
.to raise_error(ActiveRecord::RecordInvalid)
expect(comment.errors.full_messages).to include(message)
expect(comment.reload.comment_content).to eq("Hmm.")
end
end
end
shared_examples "deleting comments is allowed" do
describe "destroy_or_mark_deleted" do
before { comment.save(validate: false) }
it "allows deleting comments" do
expect(comment.destroy_or_mark_deleted).to be_truthy
expect { comment.reload }.to raise_exception ActiveRecord::RecordNotFound
end
it "allows deleting comments with replies" do
create(:comment, commentable: comment)
expect(comment.destroy_or_mark_deleted).to be_truthy
expect { comment.reload }.not_to raise_exception
expect(comment.is_deleted).to be_truthy
expect(comment.comment_content).to eq("deleted comment")
end
end
end
context "when the commenter is blocked by the work's owner" do
let(:work) { create(:work) }
let(:blocker) { work.users.first }
context "when commenting directly on the work" do
let(:commentable) { work.first_chapter }
it_behaves_like "creating and editing comments is not allowed",
message: "Sorry, you have been blocked by one or more of this work's creators."
it_behaves_like "deleting comments is allowed"
end
context "when replying to someone else's comment on the work" do
let(:commentable) { create(:comment, commentable: work.first_chapter) }
it_behaves_like "creating and editing comments is not allowed",
message: "Sorry, you have been blocked by one or more of this work's creators."
it_behaves_like "deleting comments is allowed"
end
end
context "when the commenter shares the work with their blocker" do
let(:blocker) { create(:user) }
let(:work) { create(:work, authors: [blocker.default_pseud, blocked.default_pseud]) }
context "when commenting directly on the work" do
let(:commentable) { work.first_chapter }
it_behaves_like "creating and editing comments is allowed"
it_behaves_like "deleting comments is allowed"
end
context "when replying to someone else's comment on the work" do
let(:commentable) { create(:comment, commentable: work.first_chapter) }
it_behaves_like "creating and editing comments is allowed"
it_behaves_like "deleting comments is allowed"
end
context "when replying to their blocker on their shared work" do
let(:commentable) { create(:comment, pseud: blocker.default_pseud, commentable: work.first_chapter) }
it_behaves_like "creating and editing comments is not allowed",
message: "Sorry, you have been blocked by that user."
it_behaves_like "deleting comments is allowed"
end
end
context "when the commenter is blocked by the person they're replying to" do
let(:blocker) { commentable.user }
context "on a work" do
let(:commentable) { create(:comment) }
it_behaves_like "creating and editing comments is not allowed",
message: "Sorry, you have been blocked by that user."
it_behaves_like "deleting comments is allowed"
end
context "on an admin post" do
let(:commentable) { create(:comment, :on_admin_post) }
it_behaves_like "creating and editing comments is not allowed",
message: "Sorry, you have been blocked by that user."
it_behaves_like "deleting comments is allowed"
end
context "on a tag" do
let(:commentable) { create(:comment, :on_tag) }
it_behaves_like "creating and editing comments is allowed"
it_behaves_like "deleting comments is allowed"
end
end
end
context "when user has disabled guest replies" do
let(:no_reply_guy) do
user = create(:user)
user.preference.update!(guest_replies_off: true)
user
end
let(:guest_reply) do
Comment.new(commentable: comment,
pseud: nil,
name: "unwelcome guest",
email: Faker::Internet.email,
comment_content: "I'm a vampire.")
end
let(:user_reply) do
Comment.new(commentable: comment,
pseud: create(:user).default_pseud,
comment_content: "Hmm.")
end
shared_examples "creating guest reply is allowed" do
describe "save" do
it "allows guest replies" do
expect(guest_reply.save).to be_truthy
expect(guest_reply.errors.full_messages).to be_blank
end
it "allows user replies" do
expect(user_reply.save).to be_truthy
expect(user_reply.errors.full_messages).to be_blank
end
end
end
shared_examples "creating guest reply is not allowed" do
describe "save" do
it "doesn't allow guest replies" do
expect(guest_reply.save).to be_falsey
expect(guest_reply.errors.full_messages).to include("Sorry, this user doesn't allow non-Archive users to reply to their comments.")
end
it "allows user replies" do
expect(user_reply.save).to be_truthy
expect(user_reply.errors.full_messages).to be_blank
end
end
end
context "comment on a work" do
let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud) }
include_examples "creating guest reply is not allowed"
end
context "comment on an admin post" do
let(:comment) { create(:comment, :on_admin_post, pseud: no_reply_guy.default_pseud) }
include_examples "creating guest reply is not allowed"
end
context "comment on a tag" do
let(:comment) { create(:comment, :on_tag, pseud: no_reply_guy.default_pseud) }
include_examples "creating guest reply is not allowed"
end
context "comment on the user's work" do
let(:work) { create(:work, authors: [no_reply_guy.default_pseud]) }
let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud, commentable: work.first_chapter) }
include_examples "creating guest reply is allowed"
end
context "comment on the user's co-creation" do
let(:work) { create(:work, authors: [create(:user).default_pseud, no_reply_guy.default_pseud]) }
let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud, commentable: work.first_chapter) }
include_examples "creating guest reply is allowed"
end
context "guest comment" do
let(:comment) { create(:comment, :by_guest) }
include_examples "creating guest reply is allowed"
end
end
describe "#create" do
context "as a tag wrangler" do
let(:tag_wrangler) { create(:tag_wrangler) }
shared_examples "updates last wrangling activity" do
it "tracks last wrangling activity", :frozen do
expect(tag_wrangler.last_wrangling_activity.updated_at).to eq(Time.now.utc)
end
end
context "direct parent is a tag" do
let!(:comment) { create(:comment, :on_tag, pseud: tag_wrangler.default_pseud) }
include_examples "updates last wrangling activity"
end
context "ultimate parent is indirectly a tag" do
let(:parent_comment) { create(:comment, :on_tag, pseud: parent_comment_owner.default_pseud) }
let(:parent_comment_owner) { create(:tag_wrangler) }
before { create(:comment, commentable: parent_comment, pseud: tag_wrangler.default_pseud) }
include_examples "updates last wrangling activity"
context "when parent comment is owned by a wrangler" do
it "notifies the wrangler" do
expect do
create(:comment, commentable: parent_comment, pseud: tag_wrangler.default_pseud)
end.to change { parent_comment_owner.inbox_comments.count }
.and enqueue_mail(CommentMailer, :comment_reply_notification)
end
end
context "when parent comment is owned by a user who is no longer a wrangler" do
before { parent_comment_owner.update!(roles: []) }
it "does not notify the user" do
expect do
create(:comment, commentable: parent_comment, pseud: tag_wrangler.default_pseud)
end.to avoid_changing { parent_comment_owner.inbox_comments.count }
.and not_enqueue_mail(CommentMailer, :comment_reply_notification)
end
end
end
shared_examples "does not update last wrangling activity" do
it "does not track last wrangling activity" do
expect(tag_wrangler.last_wrangling_activity).to be_nil
end
end
context "parent is a work" do
before { create(:comment, pseud: tag_wrangler.default_pseud) }
include_examples "does not update last wrangling activity"
end
context "parent is an admin comment" do
before { create(:comment, :on_admin_post, pseud: tag_wrangler.default_pseud) }
include_examples "does not update last wrangling activity"
end
end
context "as a non-tag wrangler" do
let(:user) { create(:archivist) }
context "parent is a tag" do
before { create(:comment, :on_tag, pseud: user.default_pseud) }
it "does not update last wrangling activity" do
expect(user.last_wrangling_activity).to be_nil
end
end
end
context "as non-user" do
context "parent is a tag" do
before { create(:comment, :by_guest, :on_tag) }
it "does not update last wrangling activity" do
expect(LastWranglingActivity.all).to be_empty
end
end
end
end
describe "#update" do
context "as a tag wrangler" do
let(:tag_wrangler) { create(:tag_wrangler) }
context "direct parent is a tag" do
let!(:comment) { create(:comment, :on_tag, pseud: tag_wrangler.default_pseud) }
it "does not update last wrangling activity" do
expect do
comment.update!(comment_content: Faker::Lorem.sentence(word_count: 25))
end.not_to change { tag_wrangler.reload.last_wrangling_activity.updated_at }
end
end
context "parent is indirectly a tag" do
let(:parent_comment) { create(:comment, :on_tag, pseud: parent_comment_owner.default_pseud) }
let(:parent_comment_owner) { create(:tag_wrangler) }
let(:reply_comment) { create(:comment, commentable: parent_comment, pseud: tag_wrangler.default_pseud) }
let(:inbox_comment) { parent_comment_owner.inbox_comments.find_by(feedback_comment_id: reply_comment.id) }
context "when parent comment is owned by a wrangler" do
before { inbox_comment.update!(read: true) }
it "notifies the wrangler by email and marks inbox comment unread" do
expect do
reply_comment.update!(
comment_content: "#{reply_comment.comment_content}!",
edited_at: Time.current
)
end.to change { inbox_comment.reload.read }
.and enqueue_mail(CommentMailer, :edited_comment_reply_notification)
end
end
context "when parent comment is owned by a user who is no longer a wrangler" do
before do
inbox_comment.update!(read: true)
parent_comment_owner.update!(roles: [])
end
it "does not notify the user by email or mark the inbox comment unread" do
expect do
reply_comment.update!(
comment_content: "#{reply_comment.comment_content}!",
edited_at: Time.current
)
end.to avoid_changing { inbox_comment.reload.read }
.and not_enqueue_mail(CommentMailer, :edited_comment_reply_notification)
end
end
end
end
end
describe "#destroy" do
context "as a tag wrangler" do
let(:tag_wrangler) { create(:tag_wrangler) }
context "direct parent is a tag" do
let!(:comment) { create(:comment, :on_tag, pseud: tag_wrangler.default_pseud) }
it "does not update last wrangling activity" do
expect do
comment.destroy
end.not_to change { tag_wrangler.reload.last_wrangling_activity.updated_at }
end
end
end
end
describe "#use_image_safety_mode?" do
let(:admin_post_comment) { create(:comment, :on_admin_post) }
let(:chapter_comment) { create(:comment) }
let(:tag_comment) { create(:comment, :on_tag) }
let(:admin_post_reply) { create(:comment, commentable: admin_post_comment) }
let(:chapter_reply) { create(:comment, commentable: chapter_comment) }
let(:tag_reply) { create(:comment, commentable: tag_comment) }
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE is empty" do
it "returns false for comments and replies for all parent types" do
expect(admin_post_comment.use_image_safety_mode?).to be_falsey
expect(chapter_comment.use_image_safety_mode?).to be_falsey
expect(tag_comment.use_image_safety_mode?).to be_falsey
expect(admin_post_reply.use_image_safety_mode?).to be_falsey
expect(chapter_reply.use_image_safety_mode?).to be_falsey
expect(tag_reply.use_image_safety_mode?).to be_falsey
end
end
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE is set to something that doesn't match an existing parent type" do
before { allow(ArchiveConfig).to receive(:PARENTS_WITH_IMAGE_SAFETY_MODE).and_return(["Work"]) }
it "returns false for comments and replies for all parent types" do
expect(admin_post_comment.use_image_safety_mode?).to be_falsey
expect(chapter_comment.use_image_safety_mode?).to be_falsey
expect(tag_comment.use_image_safety_mode?).to be_falsey
expect(admin_post_reply.use_image_safety_mode?).to be_falsey
expect(chapter_reply.use_image_safety_mode?).to be_falsey
expect(tag_reply.use_image_safety_mode?).to be_falsey
end
end
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE is set to AdminPost" do
before { allow(ArchiveConfig).to receive(:PARENTS_WITH_IMAGE_SAFETY_MODE).and_return(["AdminPost"]) }
it "returns true for AdminPost comments and replies and false for Chapter and Tag comments and replies" do
expect(admin_post_comment.use_image_safety_mode?).to be_truthy
expect(admin_post_reply.use_image_safety_mode?).to be_truthy
expect(chapter_comment.use_image_safety_mode?).to be_falsey
expect(tag_comment.use_image_safety_mode?).to be_falsey
expect(chapter_reply.use_image_safety_mode?).to be_falsey
expect(tag_reply.use_image_safety_mode?).to be_falsey
end
end
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE is set to Chapter" do
before { allow(ArchiveConfig).to receive(:PARENTS_WITH_IMAGE_SAFETY_MODE).and_return(["Chapter"]) }
it "returns true for Chapter comments and false for AdminPost and Tag comments and replies" do
expect(chapter_comment.use_image_safety_mode?).to be_truthy
expect(chapter_reply.use_image_safety_mode?).to be_truthy
expect(admin_post_comment.use_image_safety_mode?).to be_falsey
expect(tag_comment.use_image_safety_mode?).to be_falsey
expect(admin_post_reply.use_image_safety_mode?).to be_falsey
expect(tag_reply.use_image_safety_mode?).to be_falsey
end
end
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE is set to Tag" do
before { allow(ArchiveConfig).to receive(:PARENTS_WITH_IMAGE_SAFETY_MODE).and_return(["Tag"]) }
it "returns true for Tag comments and replies and false for AdminPost and Chapter comments and replies" do
expect(tag_comment.use_image_safety_mode?).to be_truthy
expect(tag_reply.use_image_safety_mode?).to be_truthy
expect(admin_post_comment.use_image_safety_mode?).to be_falsey
expect(chapter_comment.use_image_safety_mode?).to be_falsey
expect(admin_post_reply.use_image_safety_mode?).to be_falsey
expect(chapter_reply.use_image_safety_mode?).to be_falsey
end
end
context "when ArchiveConfig.PARENTS_WITH_IMAGE_SAFETY_MODE includes multiple parent types" do
before { allow(ArchiveConfig).to receive(:PARENTS_WITH_IMAGE_SAFETY_MODE).and_return(%w[AdminPost Tag]) }
it "returns true for comments and replies on the listed parent types and false for the other" do
expect(admin_post_comment.use_image_safety_mode?).to be_truthy
expect(tag_comment.use_image_safety_mode?).to be_truthy
expect(admin_post_reply.use_image_safety_mode?).to be_truthy
expect(tag_reply.use_image_safety_mode?).to be_truthy
expect(chapter_comment.use_image_safety_mode?).to be_falsey
expect(chapter_reply.use_image_safety_mode?).to be_falsey
end
end
context "when the comment is from a guest" do
let(:comment) { create(:comment, :by_guest) }
it "returns true" do
expect(comment.use_image_safety_mode?).to be_truthy
end
end
end
end