require 'rails_helper' require_dependency 'post_destroyer' describe PostAction do it { is_expected.to rate_limit } let(:moderator) { Fabricate(:moderator) } let(:codinghorror) { Fabricate(:coding_horror) } let(:eviltrout) { Fabricate(:evil_trout) } let(:admin) { Fabricate(:admin) } let(:post) { Fabricate(:post) } let(:second_post) { Fabricate(:post, topic_id: post.topic_id) } let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) } def value_for(user_id, dt) GivenDailyLike.find_for(user_id, dt).pluck(:likes_given)[0] || 0 end describe "rate limits" do it "limits redo/undo" do RateLimiter.enable PostAction.act(eviltrout, post, PostActionType.types[:like]) PostAction.remove_act(eviltrout, post, PostActionType.types[:like]) PostAction.act(eviltrout, post, PostActionType.types[:like]) PostAction.remove_act(eviltrout, post, PostActionType.types[:like]) expect { PostAction.act(eviltrout, post, PostActionType.types[:like]) }.to raise_error(RateLimiter::LimitExceeded) end end describe "messaging" do it "doesn't generate title longer than 255 characters" do topic = create_topic(title: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet rutrum neque. Pellentesque suscipit vehicula facilisis. Phasellus lacus sapien, aliquam nec convallis sit amet, vestibulum laoreet ante. Curabitur et pellentesque tortor. Donec non.") post = create_post(topic: topic) expect { PostAction.act(admin, post, PostActionType.types[:notify_user], message: "WAT") }.not_to raise_error end it "notify moderators integration test" do post = create_post mod = moderator Group.refresh_automatic_groups! action = PostAction.act(codinghorror, post, PostActionType.types[:notify_moderators], message: "this is my special long message") posts = Post.joins(:topic) .select('posts.id, topics.subtype, posts.topic_id') .where('topics.archetype' => Archetype.private_message) .to_a expect(posts.count).to eq(1) expect(action.related_post_id).to eq(posts[0].id.to_i) expect(posts[0].subtype).to eq(TopicSubtype.notify_moderators) topic = posts[0].topic # Moderators should be invited to the private topic, otherwise they're not permitted to see it topic_user_ids = topic.reload.topic_users.map { |x| x.user_id } expect(topic_user_ids).to include(codinghorror.id) expect(topic_user_ids).to include(mod.id) expect(topic.topic_users.where(user_id: mod.id) .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:tracking]) expect(topic.topic_users.where(user_id: codinghorror.id) .pluck(:notification_level).first).to eq(TopicUser.notification_levels[:watching]) # reply to PM should not clear flag PostCreator.new(mod, topic_id: posts[0].topic_id, raw: "This is my test reply to the user, it should clear flags").create action.reload expect(action.deleted_at).to eq(nil) # Acting on the flag should not post an automated status message (since a moderator already replied) expect(topic.posts.count).to eq(2) PostAction.agree_flags!(post, admin) topic.reload expect(topic.posts.count).to eq(2) # Clearing the flags should not post an automated status message PostAction.act(mod, post, PostActionType.types[:notify_moderators], message: "another special message") PostAction.clear_flags!(post, admin) topic.reload expect(topic.posts.count).to eq(2) # Acting on the flag should post an automated status message another_post = create_post action = PostAction.act(codinghorror, another_post, PostActionType.types[:notify_moderators], message: "foobar") topic = action.related_post.topic expect(topic.posts.count).to eq(1) PostAction.agree_flags!(another_post, admin) topic.reload expect(topic.posts.count).to eq(2) expect(topic.posts.last.post_type).to eq(Post.types[:moderator_action]) end describe 'notify_moderators' do before do PostAction.stubs(:create) end it "creates a pm if selected" do post = build(:post, id: 1000) PostCreator.any_instance.expects(:create).returns(post) PostAction.act(build(:user), build(:post), PostActionType.types[:notify_moderators], message: "this is my special message") end end describe "notify_user" do before do PostAction.stubs(:create) post = build(:post) post.user = build(:user) end it "sends an email to user if selected" do PostCreator.any_instance.expects(:create).returns(build(:post)) PostAction.act(build(:user), post, PostActionType.types[:notify_user], message: "this is my special message") end end end describe "flag counts" do before do PostAction.update_flagged_posts_count end it "increments the numbers correctly" do expect(PostAction.flagged_posts_count).to eq(0) PostAction.act(codinghorror, post, PostActionType.types[:off_topic]) expect(PostAction.flagged_posts_count).to eq(1) PostAction.clear_flags!(post, Discourse.system_user) expect(PostAction.flagged_posts_count).to eq(0) end it "should reset counts when a topic is deleted" do PostAction.act(codinghorror, post, PostActionType.types[:off_topic]) post.topic.trash! expect(PostAction.flagged_posts_count).to eq(0) end it "should ignore flags on non-human users" do post = create_post(user: Discourse.system_user) PostAction.act(codinghorror, post, PostActionType.types[:off_topic]) expect(PostAction.flagged_posts_count).to eq(0) end it "should ignore validated flags" do post = create_post PostAction.act(codinghorror, post, PostActionType.types[:off_topic]) expect(post.hidden).to eq(false) expect(post.hidden_at).to be_blank PostAction.defer_flags!(post, admin) expect(PostAction.flagged_posts_count).to eq(0) post.reload expect(post.hidden).to eq(false) expect(post.hidden_at).to be_blank PostAction.hide_post!(post, PostActionType.types[:off_topic]) post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present end end describe "update_counters" do it "properly updates topic counters" do freeze_time Date.today # we need this to test it TopicUser.change(codinghorror, post.topic, posted: true) expect(value_for(moderator.id, Date.today)).to eq(0) PostAction.act(moderator, post, PostActionType.types[:like]) PostAction.act(codinghorror, second_post, PostActionType.types[:like]) post.topic.reload expect(post.topic.like_count).to eq(2) expect(value_for(moderator.id, Date.today)).to eq(1) tu = TopicUser.get(post.topic, codinghorror) expect(tu.liked).to be true expect(tu.bookmarked).to be false end end describe "when a user bookmarks something" do it "increases the post's bookmark count when saved" do expect { bookmark.save; post.reload }.to change(post, :bookmark_count).by(1) end describe 'when deleted' do before do bookmark.save post.reload @topic = post.topic @topic.reload bookmark.deleted_at = DateTime.now bookmark.save end it 'reduces the bookmark count of the post' do expect { post.reload }.to change(post, :bookmark_count).by(-1) end end end describe 'when a user likes something' do it 'should generate notifications correctly' do PostActionNotifier.enable PostAction.act(codinghorror, post, PostActionType.types[:like]) expect(Notification.count).to eq(1) mutee = Fabricate(:user) post = Fabricate(:post) MutedUser.create!(user_id: post.user.id, muted_user_id: mutee.id) PostAction.act(mutee, post, PostActionType.types[:like]) expect(Notification.count).to eq(1) # you can not mute admin, sorry MutedUser.create!(user_id: post.user.id, muted_user_id: admin.id) PostAction.act(admin, post, PostActionType.types[:like]) expect(Notification.count).to eq(2) end it 'should increase the `like_count` and `like_score` when a user likes something' do freeze_time Date.today PostAction.act(codinghorror, post, PostActionType.types[:like]) post.reload expect(post.like_count).to eq(1) expect(post.like_score).to eq(1) post.topic.reload expect(post.topic.like_count).to eq(1) expect(value_for(codinghorror.id, Date.today)).to eq(1) # When a staff member likes it PostAction.act(moderator, post, PostActionType.types[:like]) post.reload expect(post.like_count).to eq(2) expect(post.like_score).to eq(4) # Removing likes PostAction.remove_act(codinghorror, post, PostActionType.types[:like]) post.reload expect(post.like_count).to eq(1) expect(post.like_score).to eq(3) expect(value_for(codinghorror.id, Date.today)).to eq(0) PostAction.remove_act(moderator, post, PostActionType.types[:like]) post.reload expect(post.like_count).to eq(0) expect(post.like_score).to eq(0) end it "shouldn't change given_likes unless likes are given or removed" do freeze_time(Time.zone.now) PostAction.act(codinghorror, Fabricate(:post), PostActionType.types[:like]) expect(value_for(codinghorror.id, Date.today)).to eq(1) PostActionType.types.each do |type_name, type_id| post = Fabricate(:post) PostAction.act(codinghorror, post, type_id) actual_count = value_for(codinghorror.id, Date.today) expected_count = type_name == :like ? 2 : 1 expect(actual_count).to eq(expected_count), "Expected likes_given to be #{expected_count} when adding '#{type_name}', but got #{actual_count}" PostAction.remove_act(codinghorror, post, type_id) actual_count = value_for(codinghorror.id, Date.today) expect(actual_count).to eq(1), "Expected likes_given to be 1 when removing '#{type_name}', but got #{actual_count}" end end end describe "undo/redo repeatedly" do it "doesn't create a second action for the same user/type" do PostAction.act(codinghorror, post, PostActionType.types[:like]) PostAction.remove_act(codinghorror, post, PostActionType.types[:like]) PostAction.act(codinghorror, post, PostActionType.types[:like]) expect(PostAction.where(post: post).with_deleted.count).to eq(1) PostAction.remove_act(codinghorror, post, PostActionType.types[:like]) # Check that we don't lose consistency into negatives expect(post.reload.like_count).to eq(0) end end describe 'when a user likes something' do it 'should increase the like counts when a user votes' do expect { PostAction.act(codinghorror, post, PostActionType.types[:like]) post.reload }.to change(post, :like_count).by(1) end it 'should increase the forum topic vote count when a user votes' do expect { PostAction.act(codinghorror, post, PostActionType.types[:like]) post.topic.reload }.to change(post.topic, :like_count).by(1) expect { PostAction.remove_act(codinghorror, post, PostActionType.types[:like]) post.topic.reload }.to change(post.topic, :like_count).by(-1) end end describe 'flagging' do context "flag_counts_for" do it "returns the correct flag counts" do post = create_post SiteSetting.flags_required_to_hide_post = 7 # A post with no flags has 0 for flag counts expect(PostAction.flag_counts_for(post.id)).to eq([0, 0]) _flag = PostAction.act(eviltrout, post, PostActionType.types[:spam]) expect(PostAction.flag_counts_for(post.id)).to eq([0, 1]) # If staff takes action, it is ranked higher PostAction.act(admin, post, PostActionType.types[:spam], take_action: true) expect(PostAction.flag_counts_for(post.id)).to eq([0, 8]) # If a flag is dismissed PostAction.clear_flags!(post, admin) expect(PostAction.flag_counts_for(post.id)).to eq([8, 0]) end end it 'does not allow you to flag stuff with the same reason more than once' do post = Fabricate(:post) PostAction.act(eviltrout, post, PostActionType.types[:spam]) expect { PostAction.act(eviltrout, post, PostActionType.types[:off_topic]) }.to raise_error(PostAction::AlreadyActed) end it 'allows you to flag stuff with another reason' do post = Fabricate(:post) PostAction.act(eviltrout, post, PostActionType.types[:spam]) PostAction.remove_act(eviltrout, post, PostActionType.types[:spam]) expect { PostAction.act(eviltrout, post, PostActionType.types[:off_topic]) }.not_to raise_error() end it 'should update counts when you clear flags' do post = Fabricate(:post) PostAction.act(eviltrout, post, PostActionType.types[:spam]) post.reload expect(post.spam_count).to eq(1) PostAction.clear_flags!(post, Discourse.system_user) post.reload expect(post.spam_count).to eq(0) end it "will not allow regular users to auto hide staff posts" do mod = Fabricate(:moderator) post = Fabricate(:post, user: mod) SiteSetting.flags_required_to_hide_post = 2 Discourse.stubs(:site_contact_user).returns(admin) PostAction.act(eviltrout, post, PostActionType.types[:spam]) PostAction.act(Fabricate(:walter_white), post, PostActionType.types[:spam]) post.reload expect(post.hidden).to eq(false) expect(post.hidden_at).to be_blank end it "allows staff users to auto hide staff posts" do mod = Fabricate(:moderator) post = Fabricate(:post, user: mod) SiteSetting.flags_required_to_hide_post = 2 Discourse.stubs(:site_contact_user).returns(admin) PostAction.act(eviltrout, post, PostActionType.types[:spam]) PostAction.act(Fabricate(:admin), post, PostActionType.types[:spam]) post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present end it 'should follow the rules for automatic hiding workflow' do post = create_post walterwhite = Fabricate(:walter_white) SiteSetting.queue_jobs = true SiteSetting.flags_required_to_hide_post = 2 Discourse.stubs(:site_contact_user).returns(admin) PostAction.act(eviltrout, post, PostActionType.types[:spam]) PostAction.act(walterwhite, post, PostActionType.types[:spam]) job_args = Jobs::SendSystemMessage.jobs.last["args"].first expect(job_args["user_id"]).to eq(post.user.id) expect(job_args["message_type"]).to eq("post_hidden") post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached]) expect(post.topic.visible).to eq(false) post.revise(post.user, raw: post.raw + " ha I edited it ") post.reload expect(post.hidden).to eq(false) expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached]) # keep most recent reason expect(post.hidden_at).to be_present # keep the most recent hidden_at time expect(post.topic.visible).to eq(true) PostAction.act(eviltrout, post, PostActionType.types[:spam]) PostAction.act(walterwhite, post, PostActionType.types[:off_topic]) job_args = Jobs::SendSystemMessage.jobs.last["args"].first expect(job_args["user_id"]).to eq(post.user.id) expect(job_args["message_type"]).to eq("post_hidden_again") post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached_again]) expect(post.topic.visible).to eq(false) post.revise(post.user, raw: post.raw + " ha I edited it again ") post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flag_threshold_reached_again]) expect(post.topic.visible).to eq(false) end it "hide tl0 posts that are flagged as spam by a tl3 user" do newuser = Fabricate(:newuser) post = create_post(user: newuser) Discourse.stubs(:site_contact_user).returns(admin) PostAction.act(Fabricate(:leader), post, PostActionType.types[:spam]) post.reload expect(post.hidden).to eq(true) expect(post.hidden_at).to be_present expect(post.hidden_reason_id).to eq(Post.hidden_reasons[:flagged_by_tl3_user]) end it "can flag the topic instead of a post" do post1 = create_post _post2 = create_post(topic: post1.topic) post_action = PostAction.act(Fabricate(:user), post1, PostActionType.types[:spam], flag_topic: true) expect(post_action.targets_topic).to eq(true) end it "will flag the first post if you flag a topic but there is only one post in the topic" do post = create_post post_action = PostAction.act(Fabricate(:user), post, PostActionType.types[:spam], flag_topic: true) expect(post_action.targets_topic).to eq(false) expect(post_action.post_id).to eq(post.id) end it "will unhide the post when a moderator undos the flag on which s/he took action" do Discourse.stubs(:site_contact_user).returns(admin) post = create_post PostAction.act(moderator, post, PostActionType.types[:spam], take_action: true) post.reload expect(post.hidden).to eq(true) PostAction.remove_act(moderator, post, PostActionType.types[:spam]) post.reload expect(post.hidden).to eq(false) end it "will automatically pause a topic due to large community flagging" do SiteSetting.flags_required_to_hide_post = 0 SiteSetting.num_flags_to_close_topic = 3 SiteSetting.num_flaggers_to_close_topic = 2 SiteSetting.num_hours_to_close_topic = 1 topic = Fabricate(:topic) post1 = create_post(topic: topic) post2 = create_post(topic: topic) post3 = create_post(topic: topic) flagger1 = Fabricate(:user) flagger2 = Fabricate(:user) # reaching `num_flaggers_to_close_topic` isn't enough [flagger1, flagger2].each do |flagger| PostAction.act(flagger, post1, PostActionType.types[:inappropriate]) end expect(topic.reload.closed).to eq(false) # clean up PostAction.where(post: post1).delete_all # reaching `num_flags_to_close_topic` isn't enough [post1, post2, post3].each do |post| PostAction.act(flagger1, post, PostActionType.types[:inappropriate]) end expect(topic.reload.closed).to eq(false) # clean up PostAction.where(post: [post1, post2, post3]).delete_all # reaching both should close the topic [flagger1, flagger2].each do |flagger| [post1, post2, post3].each do |post| PostAction.act(flagger, post, PostActionType.types[:inappropriate]) end end expect(topic.reload.closed).to eq(true) topic_status_update = TopicTimer.last expect(topic_status_update.topic).to eq(topic) expect(topic_status_update.execute_at).to be_within(1.second).of(1.hour.from_now) expect(topic_status_update.status_type).to eq(TopicTimer.types[:open]) end end it "prevents user to act twice at the same time" do # flags are already being tested all_types_except_flags = PostActionType.types.except(PostActionType.flag_types_without_custom) all_types_except_flags.values.each do |action| expect do PostAction.act(eviltrout, post, action) PostAction.act(eviltrout, post, action) end.to raise_error(PostAction::AlreadyActed) end end describe ".create_message_for_post_action" do it "does not create a message when there is no message" do message_id = PostAction.create_message_for_post_action(Discourse.system_user, post, PostActionType.types[:spam], {}) expect(message_id).to be_nil end [:notify_moderators, :notify_user, :spam].each do |post_action_type| it "creates a message for #{post_action_type}" do message_id = PostAction.create_message_for_post_action(Discourse.system_user, post, PostActionType.types[post_action_type], message: "WAT") expect(message_id).to be_present end end it "should raise the right errors when it fails to create a post" do begin group = Group[:moderators] messageable_level = group.messageable_level group.update!(messageable_level: Group::ALIAS_LEVELS[:nobody]) expect do PostAction.create_message_for_post_action( Fabricate(:user), post, PostActionType.types[:notify_moderators], message: 'testing', ) end.to raise_error(ActiveRecord::RecordNotSaved) ensure group.update!(messageable_level: messageable_level) end end end describe ".lookup_for" do it "returns the correct map" do user = Fabricate(:user) post = Fabricate(:post) post_action = PostAction.create(user_id: user.id, post_id: post.id, post_action_type_id: 1) map = PostAction.lookup_for(user, [post.topic], post_action.post_action_type_id) expect(map).to eq(post.topic_id => [post.post_number]) end end describe "#add_moderator_post_if_needed" do it "should not add a moderator post when it's disabled" do post = create_post action = PostAction.act(moderator, post, PostActionType.types[:spam], message: "WAT") action.reload topic = action.related_post.topic expect(topic.posts.count).to eq(1) SiteSetting.auto_respond_to_flag_actions = false PostAction.agree_flags!(post, admin) topic.reload expect(topic.posts.count).to eq(1) end it "should create a notification in the related topic" do post = Fabricate(:post) user = Fabricate(:user) action = PostAction.act(user, post, PostActionType.types[:spam], message: "WAT") topic = action.reload.related_post.topic expect(user.notifications.count).to eq(0) SiteSetting.auto_respond_to_flag_actions = true PostAction.agree_flags!(post, admin) user_notifications = user.notifications expect(user_notifications.count).to eq(1) expect(user_notifications.last.topic).to eq(topic) end end describe "rate limiting" do def limiter(tl) user = Fabricate.build(:user) user.trust_level = tl action = PostAction.new(user: user, post_action_type_id: PostActionType.types[:like]) action.post_action_rate_limiter end it "should scale up like limits depending on liker" do expect(limiter(0).max).to eq SiteSetting.max_likes_per_day expect(limiter(1).max).to eq SiteSetting.max_likes_per_day expect(limiter(2).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl2_additional_likes_per_day_multiplier).to_i expect(limiter(3).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl3_additional_likes_per_day_multiplier).to_i expect(limiter(4).max).to eq (SiteSetting.max_likes_per_day * SiteSetting.tl4_additional_likes_per_day_multiplier).to_i SiteSetting.tl2_additional_likes_per_day_multiplier = -1 expect(limiter(2).max).to eq SiteSetting.max_likes_per_day SiteSetting.tl2_additional_likes_per_day_multiplier = 0.8 expect(limiter(2).max).to eq SiteSetting.max_likes_per_day SiteSetting.tl2_additional_likes_per_day_multiplier = "bob" expect(limiter(2).max).to eq SiteSetting.max_likes_per_day end end describe '#is_flag?' do describe 'when post action is a flag' do it 'should return true' do PostActionType.notify_flag_types.each do |_type, id| post_action = PostAction.new( user: codinghorror, post_action_type_id: id ) expect(post_action.is_flag?).to eq(true) end end end describe 'when post action is not a flag' do it 'should return false' do post_action = PostAction.new( user: codinghorror, post_action_type_id: 99 ) expect(post_action.is_flag?).to eq(false) end end end describe "triggers webhook events" do let(:post) { Fabricate(:post) } it 'flag created' do event = DiscourseEvent.track_events { PostAction.act(eviltrout, post, PostActionType.types[:spam]) }.last expect(event[:event_name]).to eq(:flag_created) end context "resolving flags" do before do @flag = PostAction.act(eviltrout, post, PostActionType.types[:spam]) end it 'flag agreed' do event = DiscourseEvent.track_events { PostAction.agree_flags!(post, moderator) }.last expect(event[:event_name]).to eq(:flag_agreed) expect(event[:params].first).to eq(@flag) end it 'flag disagreed' do event = DiscourseEvent.track_events { PostAction.clear_flags!(post, moderator) }.last expect(event[:event_name]).to eq(:flag_disagreed) expect(event[:params].first).to eq(@flag) end it 'flag deferred' do event = DiscourseEvent.track_events { PostAction.defer_flags!(post, moderator) }.last expect(event[:event_name]).to eq(:flag_deferred) expect(event[:params].first).to eq(@flag) end end end end