FIX: Add DB constraints for post & topic counter cache for `UserStat` (#15626)

Ensures that `UserStat#post_count` and `UserStat#topic_count` does not
go below 0. When it does like it did now, we tend to have bugs in our
code since we're usually coding with the assumption that the count isn't
negative.

In order to support the constraints, our post and topic fabricators in
tests will now automatically increment the count for the respective
user's `UserStat` as well. We have to do this because our fabricators
bypasss `PostCreator` which holds the responsibility of updating `UserStat#post_count` and
`UserStat#topic_count`.
This commit is contained in:
Alan Guo Xiang Tan 2022-02-07 11:23:34 +08:00 committed by GitHub
parent 81e175e6ba
commit 5bd55acf83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 167 additions and 113 deletions

View File

@ -63,7 +63,7 @@ class TopicConverter
private private
def posters def posters
@posters ||= @topic.posts.distinct.pluck(:user_id).to_a @posters ||= @topic.posts.where("post_number > 1").distinct.pluck(:user_id)
end end
def update_user_stats def update_user_stats

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class UserStatCountUpdater
class << self
def increment!(post, user_stat: nil)
update!(post, user_stat: user_stat)
end
def decrement!(post, user_stat: nil)
update!(post, user_stat: user_stat, action: :decrement!)
end
private
def update!(post, user_stat: nil, action: :increment!)
return if !post.topic
return if post.topic.private_message?
stat = user_stat || post.user.user_stat
if post.is_first_post?
stat.public_send(action, :topic_count)
elsif post.post_type == Post.types[:regular]
stat.public_send(action, :post_count)
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddConstraintsToUserStat < ActiveRecord::Migration[6.1]
def up
execute(<<~SQL)
UPDATE user_stats
SET post_count = 0
WHERE post_count < 0
SQL
execute(<<~SQL)
UPDATE user_stats
SET topic_count = 0
WHERE topic_count < 0
SQL
execute "ALTER TABLE user_stats ADD CONSTRAINT topic_count_positive CHECK (topic_count >= 0)"
execute "ALTER TABLE user_stats ADD CONSTRAINT post_count_positive CHECK (post_count >= 0)"
end
def down
execute "ALTER TABLE user_stats DROP CONSTRAINT topic_count_positive"
execute "ALTER TABLE user_stats DROP CONSTRAINT post_count_positive"
end
end

View File

@ -60,6 +60,7 @@ module UserGuardian
def can_delete_user?(user) def can_delete_user?(user)
return false if user.nil? || user.admin? return false if user.nil? || user.admin?
if is_me?(user) if is_me?(user)
!SiteSetting.enable_discourse_connect && !SiteSetting.enable_discourse_connect &&
!user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count)

View File

@ -599,15 +599,10 @@ class PostCreator
@user.create_user_stat if @user.user_stat.nil? @user.create_user_stat if @user.user_stat.nil?
if @user.user_stat.first_post_created_at.nil? if @user.user_stat.first_post_created_at.nil?
@user.user_stat.first_post_created_at = @post.created_at @user.user_stat.update!(first_post_created_at: @post.created_at)
end end
unless @post.topic.private_message? UserStatCountUpdater.increment!(@post)
@user.user_stat.post_count += 1 if @post.post_type == Post.types[:regular] && !@post.is_first_post?
@user.user_stat.topic_count += 1 if @post.is_first_post?
end
@user.user_stat.save!
if !@topic.private_message? && @post.post_type != Post.types[:whisper] if !@topic.private_message? && @post.post_type != Post.types[:whisper]
@user.update(last_posted_at: @post.created_at) @user.update(last_posted_at: @post.created_at)

View File

@ -155,8 +155,9 @@ class PostDestroyer
make_previous_post_the_last_one make_previous_post_the_last_one
mark_topic_changed mark_topic_changed
clear_user_posted_flag clear_user_posted_flag
Topic.reset_highest(@post.topic_id)
end end
Topic.reset_highest(@post.topic_id)
trash_public_post_actions trash_public_post_actions
trash_revisions trash_revisions
trash_user_actions trash_user_actions
@ -177,7 +178,7 @@ class PostDestroyer
end end
TopicLink.where(link_post_id: @post.id).destroy_all TopicLink.where(link_post_id: @post.id).destroy_all
update_associated_category_latest_topic update_associated_category_latest_topic
update_user_counts update_user_counts if !permanent?
TopicUser.update_post_action_cache(post_id: @post.id) TopicUser.update_post_action_cache(post_id: @post.id)
DB.after_commit do DB.after_commit do
@ -387,17 +388,10 @@ class PostDestroyer
author.create_user_stat if author.user_stat.nil? author.create_user_stat if author.user_stat.nil?
if @post.created_at == author.user_stat.first_post_created_at if @post.created_at == author.user_stat.first_post_created_at
author.user_stat.first_post_created_at = author.posts.order('created_at ASC').first.try(:created_at) author.user_stat.update!(first_post_created_at: author.posts.order('created_at ASC').first.try(:created_at))
end end
if @post.topic && !@post.topic.private_message? UserStatCountUpdater.decrement!(@post)
if @post.post_type == Post.types[:regular] && !@post.is_first_post? && !@topic.nil?
author.user_stat.post_count -= 1
end
author.user_stat.topic_count -= 1 if @post.is_first_post?
end
author.user_stat.save!
if @post.created_at == author.last_posted_at if @post.created_at == author.last_posted_at
author.last_posted_at = author.posts.order('created_at DESC').first.try(:created_at) author.last_posted_at = author.posts.order('created_at DESC').first.try(:created_at)
@ -407,6 +401,7 @@ class PostDestroyer
if @post.is_first_post? && @post.topic && !@post.topic.private_message? if @post.is_first_post? && @post.topic && !@post.topic.private_message?
# Update stats of all people who replied # Update stats of all people who replied
counts = Post.where(post_type: Post.types[:regular], topic_id: @post.topic_id).where('post_number > 1').group(:user_id).count counts = Post.where(post_type: Post.types[:regular], topic_id: @post.topic_id).where('post_number > 1').group(:user_id).count
counts.each do |user_id, count| counts.each do |user_id, count|
if user_stat = UserStat.where(user_id: user_id).first if user_stat = UserStat.where(user_id: user_id).first
user_stat.update(post_count: user_stat.post_count - count) user_stat.update(post_count: user_stat.post_count - count)

View File

@ -437,22 +437,19 @@ class PostRevisor
private_message = @topic.private_message? private_message = @topic.private_message?
prev_owner_user_stat = prev_owner.user_stat prev_owner_user_stat = prev_owner.user_stat
unless private_message unless private_message
prev_owner_user_stat.post_count -= 1 if @post.post_type == Post.types[:regular] UserStatCountUpdater.decrement!(@post, user_stat: prev_owner_user_stat) if !@post.trashed?
prev_owner_user_stat.topic_count -= 1 if @post.is_first_post?
prev_owner_user_stat.likes_received -= likes prev_owner_user_stat.likes_received -= likes
end end
if @post.created_at == prev_owner.user_stat.first_post_created_at if @post.created_at == prev_owner.user_stat.first_post_created_at
prev_owner_user_stat.first_post_created_at = prev_owner.posts.order('created_at ASC').first.try(:created_at) prev_owner_user_stat.update!(first_post_created_at: prev_owner.posts.order('created_at ASC').first.try(:created_at))
end end
prev_owner_user_stat.save!
new_owner_user_stat = new_owner.user_stat new_owner_user_stat = new_owner.user_stat
unless private_message unless private_message
new_owner_user_stat.post_count += 1 if @post.post_type == Post.types[:regular] UserStatCountUpdater.increment!(@post, user_stat: new_owner_user_stat) if !@post.trashed?
new_owner_user_stat.topic_count += 1 if @post.is_first_post?
new_owner_user_stat.likes_received += likes new_owner_user_stat.likes_received += likes
end end
new_owner_user_stat.save! new_owner_user_stat.save!

View File

@ -243,10 +243,11 @@ describe UserGuardian do
end end
it "isn't allowed when user created too many posts" do it "isn't allowed when user created too many posts" do
Fabricate(:post, user: user) topic = Fabricate(:topic)
Fabricate(:post, topic: topic, user: user)
expect(guardian.can_delete_user?(user)).to eq(true) expect(guardian.can_delete_user?(user)).to eq(true)
Fabricate(:post, user: user) Fabricate(:post, topic: topic, user: user)
expect(guardian.can_delete_user?(user)).to eq(false) expect(guardian.can_delete_user?(user)).to eq(false)
end end
@ -319,16 +320,18 @@ describe UserGuardian do
end end
it "correctly respects the delete_user_self_max_post_count setting" do it "correctly respects the delete_user_self_max_post_count setting" do
topic = Fabricate(:topic)
SiteSetting.delete_user_self_max_post_count = 0 SiteSetting.delete_user_self_max_post_count = 0
expect(guardian.can_delete_user?(user)).to eq(true) expect(guardian.can_delete_user?(user)).to eq(true)
Fabricate(:post, user: user) Fabricate(:post, topic: topic, user: user)
expect(guardian.can_delete_user?(user)).to eq(false) expect(guardian.can_delete_user?(user)).to eq(false)
SiteSetting.delete_user_self_max_post_count = 1 SiteSetting.delete_user_self_max_post_count = 1
expect(guardian.can_delete_user?(user)).to eq(true) expect(guardian.can_delete_user?(user)).to eq(true)
Fabricate(:post, user: user) Fabricate(:post, topic: topic, user: user)
expect(guardian.can_delete_user?(user)).to eq(false) expect(guardian.can_delete_user?(user)).to eq(false)
SiteSetting.delete_user_self_max_post_count = 2 SiteSetting.delete_user_self_max_post_count = 2

View File

@ -1184,6 +1184,8 @@ describe Guardian do
end end
describe "can_recover_topic?" do describe "can_recover_topic?" do
fab!(:topic) { Fabricate(:topic, user: user) }
fab!(:post) { Fabricate(:post, user: user, topic: topic) }
it "returns false for a nil user" do it "returns false for a nil user" do
expect(Guardian.new(nil).can_recover_topic?(topic)).to be_falsey expect(Guardian.new(nil).can_recover_topic?(topic)).to be_falsey
@ -1198,11 +1200,6 @@ describe Guardian do
end end
context 'as a moderator' do context 'as a moderator' do
before do
topic.save!
post.save!
end
describe 'when post has been deleted' do describe 'when post has been deleted' do
it "should return the right value" do it "should return the right value" do
expect(Guardian.new(moderator).can_recover_topic?(topic)).to be_falsey expect(Guardian.new(moderator).can_recover_topic?(topic)).to be_falsey
@ -1227,9 +1224,6 @@ describe Guardian do
fab!(:group_user) { Fabricate(:group_user) } fab!(:group_user) { Fabricate(:group_user) }
before do before do
topic.save!
post.save!
SiteSetting.enable_category_group_moderation = true SiteSetting.enable_category_group_moderation = true
PostDestroyer.new(moderator, topic.first_post).destroy PostDestroyer.new(moderator, topic.first_post).destroy
topic.reload topic.reload
@ -1262,10 +1256,8 @@ describe Guardian do
end end
context 'as a moderator' do context 'as a moderator' do
before do fab!(:topic) { Fabricate(:topic, user: user) }
topic.save! fab!(:post) { Fabricate(:post, user: user, topic: topic) }
post.save!
end
describe 'when post has been deleted' do describe 'when post has been deleted' do
it "should return the right value" do it "should return the right value" do

View File

@ -4,12 +4,12 @@ require 'rails_helper'
require 'new_post_manager' require 'new_post_manager'
describe NewPostManager do describe NewPostManager do
fab!(:user) { Fabricate(:user) }
fab!(:topic) { Fabricate(:topic) } fab!(:topic) { Fabricate(:topic) }
context "default action" do context "default action" do
it "creates the post by default" do it "creates the post by default" do
manager = NewPostManager.new(topic.user, raw: 'this is a new post', topic_id: topic.id) manager = NewPostManager.new(user, raw: 'this is a new post', topic_id: topic.id)
result = manager.perform result = manager.perform
expect(result.action).to eq(:create_post) expect(result.action).to eq(:create_post)
@ -25,7 +25,7 @@ describe NewPostManager do
it "doesn't enqueue private messages" do it "doesn't enqueue private messages" do
SiteSetting.approve_unless_trust_level = 4 SiteSetting.approve_unless_trust_level = 4
manager = NewPostManager.new(topic.user, manager = NewPostManager.new(user,
raw: 'this is a new post', raw: 'this is a new post',
title: 'this is a new title', title: 'this is a new title',
archetype: Archetype.private_message, archetype: Archetype.private_message,
@ -40,7 +40,7 @@ describe NewPostManager do
expect(result.post).to be_a(Post) expect(result.post).to be_a(Post)
# It doesn't enqueue replies to the private message either # It doesn't enqueue replies to the private message either
manager = NewPostManager.new(topic.user, manager = NewPostManager.new(user,
raw: 'this is a new reply', raw: 'this is a new reply',
topic_id: result.post.topic_id) topic_id: result.post.topic_id)
@ -56,7 +56,7 @@ describe NewPostManager do
end end
context "default handler" do context "default handler" do
let(:manager) { NewPostManager.new(topic.user, raw: 'this is new post content', topic_id: topic.id) } let(:manager) { NewPostManager.new(user, raw: 'this is new post content', topic_id: topic.id) }
context 'with the settings zeroed out' do context 'with the settings zeroed out' do
before do before do
@ -126,8 +126,9 @@ describe NewPostManager do
context 'with a high approval post count, but TL2' do context 'with a high approval post count, but TL2' do
before do before do
SiteSetting.approve_post_count = 100 SiteSetting.approve_post_count = 100
topic.user.trust_level = 2 user.update!(trust_level: 2)
end end
it "will return an enqueue result" do it "will return an enqueue result" do
result = NewPostManager.default_handler(manager) result = NewPostManager.default_handler(manager)
expect(result).to be_nil expect(result).to be_nil
@ -188,8 +189,9 @@ describe NewPostManager do
context 'with staged moderation setting enabled' do context 'with staged moderation setting enabled' do
before do before do
SiteSetting.approve_unless_staged = true SiteSetting.approve_unless_staged = true
topic.user.staged = true user.update!(staged: true)
end end
it "will return an enqueue result" do it "will return an enqueue result" do
result = NewPostManager.default_handler(manager) result = NewPostManager.default_handler(manager)
expect(NewPostManager.queue_enabled?).to eq(true) expect(NewPostManager.queue_enabled?).to eq(true)
@ -209,17 +211,17 @@ describe NewPostManager do
end end
context 'with a fast typer' do context 'with a fast typer' do
let(:user) { manager.user }
before do before do
user.update!(trust_level: 0) user.update!(trust_level: 0)
end end
it "adds the silence reason in the system locale" do it "adds the silence reason in the system locale" do
manager = build_manager_with('this is new post content') manager = build_manager_with('this is new post content')
I18n.with_locale(:fr) do # Simulate french user I18n.with_locale(:fr) do # Simulate french user
result = NewPostManager.default_handler(manager) result = NewPostManager.default_handler(manager)
end end
expect(user.silenced?).to eq(true) expect(user.silenced?).to eq(true)
expect(user.silence_reason).to eq(I18n.t("user.new_user_typed_too_fast", locale: :en)) expect(user.silence_reason).to eq(I18n.t("user.new_user_typed_too_fast", locale: :en))
end end
@ -235,12 +237,11 @@ describe NewPostManager do
end end
def build_manager_with(raw) def build_manager_with(raw)
NewPostManager.new(topic.user, raw: raw, topic_id: topic.id, first_post_checks: true) NewPostManager.new(user, raw: raw, topic_id: topic.id, first_post_checks: true)
end end
end end
context 'with media' do context 'with media' do
let(:user) { manager.user }
let(:manager_opts) do let(:manager_opts) do
{ {
raw: 'this is new post content', topic_id: topic.id, first_post_checks: false, raw: 'this is new post content', topic_id: topic.id, first_post_checks: false,
@ -258,7 +259,7 @@ describe NewPostManager do
it 'queues the post for review because if it contains embedded media.' do it 'queues the post for review because if it contains embedded media.' do
SiteSetting.review_media_unless_trust_level = 1 SiteSetting.review_media_unless_trust_level = 1
manager = NewPostManager.new(topic.user, manager_opts) manager = NewPostManager.new(user, manager_opts)
result = NewPostManager.default_handler(manager) result = NewPostManager.default_handler(manager)
@ -268,7 +269,7 @@ describe NewPostManager do
it 'does not enqueue the post if the poster is a trusted user' do it 'does not enqueue the post if the poster is a trusted user' do
SiteSetting.review_media_unless_trust_level = 0 SiteSetting.review_media_unless_trust_level = 0
manager = NewPostManager.new(topic.user, manager_opts) manager = NewPostManager.new(user, manager_opts)
result = NewPostManager.default_handler(manager) result = NewPostManager.default_handler(manager)
@ -278,7 +279,7 @@ describe NewPostManager do
end end
context "new topic handler" do context "new topic handler" do
let(:manager) { NewPostManager.new(topic.user, raw: 'this is new topic content', title: 'new topic title') } let(:manager) { NewPostManager.new(user, raw: 'this is new topic content', title: 'new topic title') }
context 'with a high trust level setting for new topics' do context 'with a high trust level setting for new topics' do
before do before do
SiteSetting.approve_new_topics_unless_trust_level = 4 SiteSetting.approve_new_topics_unless_trust_level = 4
@ -351,7 +352,7 @@ describe NewPostManager do
end end
it "calls custom handlers" do it "calls custom handlers" do
manager = NewPostManager.new(topic.user, raw: 'this post increases counter', topic_id: topic.id) manager = NewPostManager.new(user, raw: 'this post increases counter', topic_id: topic.id)
result = manager.perform result = manager.perform
@ -409,7 +410,7 @@ describe NewPostManager do
end end
it "if nothing returns a result it creates a post" do it "if nothing returns a result it creates a post" do
manager = NewPostManager.new(topic.user, raw: 'this is a new post', topic_id: topic.id) manager = NewPostManager.new(user, raw: 'this is a new post', topic_id: topic.id)
result = manager.perform result = manager.perform

View File

@ -995,7 +995,7 @@ describe PostDestroyer do
end end
it 'should destroy internal links when moderator deletes post' do it 'should destroy internal links when moderator deletes post' do
new_post = Post.create!(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n") new_post = create_post(user: user, topic: topic, raw: "Link to other topic:\n\n#{url}\n")
TopicLink.extract_from(new_post) TopicLink.extract_from(new_post)
link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post]) link_counts = TopicLink.counts_for(guardian, other_topic.reload, [other_post])
expect(link_counts.count).to eq(1) expect(link_counts.count).to eq(1)

View File

@ -5,6 +5,11 @@ Fabricator(:post) do
topic { |attrs| Fabricate(:topic, user: attrs[:user]) } topic { |attrs| Fabricate(:topic, user: attrs[:user]) }
raw "Hello world" raw "Hello world"
post_type Post.types[:regular] post_type Post.types[:regular]
# Fabrication bypasses PostCreator, for performance reasons, where the counts are updated so we have to handle this manually here.
after_save do |post, _transients|
UserStatCountUpdater.increment!(post)
end
end end
Fabricator(:post_with_long_raw_content, from: :post) do Fabricator(:post_with_long_raw_content, from: :post) do

View File

@ -6,6 +6,13 @@ Fabricator(:topic) do
category_id do |attrs| category_id do |attrs|
attrs[:category] ? attrs[:category].id : SiteSetting.uncategorized_category_id attrs[:category] ? attrs[:category].id : SiteSetting.uncategorized_category_id
end end
# Fabrication bypasses PostCreator, for performance reasons, where the counts are updated so we have to handle this manually here.
after_save do |topic, _transients|
if !topic.private_message?
topic.user.user_stat.increment!(:topic_count)
end
end
end end
Fabricator(:deleted_topic, from: :topic) do Fabricator(:deleted_topic, from: :topic) do

View File

@ -68,7 +68,7 @@ describe TopicConverter do
expect(private_message.user.user_stat.post_count).to eq(0) expect(private_message.user.user_stat.post_count).to eq(0)
private_message.convert_to_public_topic(admin) private_message.convert_to_public_topic(admin)
expect(private_message.reload.user.user_stat.topic_count).to eq(1) expect(private_message.reload.user.user_stat.topic_count).to eq(1)
expect(private_message.user.user_stat.post_count).to eq(1) expect(private_message.user.user_stat.post_count).to eq(0)
expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching]) expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching])
end end

View File

@ -83,15 +83,14 @@ describe TopicLink do
it "extracts onebox" do it "extracts onebox" do
other_topic = Fabricate(:topic, user: user) other_topic = Fabricate(:topic, user: user)
other_topic.posts.create(user: user, raw: "some content for the first post") Fabricate(:post, topic: other_topic, user: user, raw: "some content for the first post")
other_post = other_topic.posts.create(user: user, raw: "some content for the second post") other_post = Fabricate(:post, topic: other_topic, user: user, raw: "some content for the second post")
url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}/#{other_post.post_number}" url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}/#{other_post.post_number}"
invalid_url = "http://#{test_uri.host}/t/#{other_topic.slug}/9999999999999999999999999999999" invalid_url = "http://#{test_uri.host}/t/#{other_topic.slug}/9999999999999999999999999999999"
topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: topic, user: user, raw: 'initial post')
post = topic.posts.create(user: user, raw: "Link to another topic:\n\n#{url}\n\n#{invalid_url}") post = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic:\n\n#{url}\n\n#{invalid_url}")
post.reload
TopicLink.extract_from(post) TopicLink.extract_from(post)
@ -110,7 +109,7 @@ describe TopicLink do
fab!(:moderator) { Fabricate(:moderator) } fab!(:moderator) { Fabricate(:moderator) }
let(:post) do let(:post) do
other_topic.posts.create(user: user, raw: "some content") Fabricate(:post, topic: other_topic, user: user, raw: "some content")
end end
it 'works' do it 'works' do
@ -120,8 +119,8 @@ describe TopicLink do
url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}" url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: topic, user: user, raw: 'initial post')
linked_post = topic.posts.create(user: user, raw: "Link to another topic: #{url}") linked_post = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{url}")
# this is subtle, but we had a bug were second time # this is subtle, but we had a bug were second time
# TopicLink.extract_from was called a reflection was nuked # TopicLink.extract_from was called a reflection was nuked
@ -169,8 +168,8 @@ describe TopicLink do
it 'works without id' do it 'works without id' do
post post
url = "http://#{test_uri.host}/t/#{other_topic.slug}" url = "http://#{test_uri.host}/t/#{other_topic.slug}"
topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: topic, user: user, raw: 'initial post')
linked_post = topic.posts.create(user: user, raw: "Link to another topic: #{url}") linked_post = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(linked_post) TopicLink.extract_from(linked_post)
link = topic.topic_links.first link = topic.topic_links.first
@ -191,8 +190,8 @@ describe TopicLink do
post post
url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}" url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: topic, user: user, raw: 'initial post')
linked_post = topic.posts.create(user: user, raw: "Link to another topic: #{url}") linked_post = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(linked_post) TopicLink.extract_from(linked_post)
expect(other_topic.reload.topic_links.where(link_post_id: linked_post.id).count).to eq(1) expect(other_topic.reload.topic_links.where(link_post_id: linked_post.id).count).to eq(1)
@ -207,10 +206,11 @@ describe TopicLink do
long_title = "Καλημερα σε ολους και ολες" * 9 # 234 chars, but the encoded slug will be 1224 chars in length long_title = "Καλημερα σε ολους και ολες" * 9 # 234 chars, but the encoded slug will be 1224 chars in length
other_topic = Fabricate(:topic, user: user, title: long_title) other_topic = Fabricate(:topic, user: user, title: long_title)
expect(other_topic.slug.length).to be > TopicLink.max_url_length expect(other_topic.slug.length).to be > TopicLink.max_url_length
other_topic.posts.create(user: user, raw: 'initial post')
Fabricate(:post, topic: other_topic, user: user, raw: 'initial post')
other_topic_url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}" other_topic_url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
post_with_link = topic.posts.create(user: user, raw: "Link to another topic: #{other_topic_url}") post_with_link = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{other_topic_url}")
TopicLink.extract_from(post_with_link) TopicLink.extract_from(post_with_link)
topic.reload topic.reload
link = topic.topic_links.first link = topic.topic_links.first
@ -226,10 +226,10 @@ describe TopicLink do
topic_url = "http://#{test_uri.host}/t/#{topic.slug}/#{topic.id}" topic_url = "http://#{test_uri.host}/t/#{topic.slug}/#{topic.id}"
other_topic = Fabricate(:topic, user: user) other_topic = Fabricate(:topic, user: user)
other_topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: other_topic, user: user, raw: 'initial post')
other_topic_url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}" other_topic_url = "http://#{test_uri.host}/t/#{other_topic.slug}/#{other_topic.id}"
post_with_link = topic.posts.create(user: user, raw: "Link to another topic: #{other_topic_url}") post_with_link = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{other_topic_url}")
expect { TopicLink.extract_from(post_with_link) }.to_not raise_error expect { TopicLink.extract_from(post_with_link) }.to_not raise_error
other_topic.reload other_topic.reload
@ -240,7 +240,8 @@ describe TopicLink do
end end
context "link to a user on discourse" do context "link to a user on discourse" do
let(:post) { topic.posts.create(user: user, raw: "<a href='/u/#{user.username_lower}'>user</a>") } let(:post) { Fabricate(:post, topic: topic, user: user, raw: "<a href='/u/#{user.username_lower}'>user</a>") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
end end
@ -251,7 +252,7 @@ describe TopicLink do
end end
context "link to a discourse resource like a FAQ" do context "link to a discourse resource like a FAQ" do
let(:post) { topic.posts.create(user: user, raw: "<a href='/faq'>faq link here</a>") } let(:post) { Fabricate(:post, topic: topic, user: user, raw: "<a href='/faq'>faq link here</a>") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
end end
@ -262,7 +263,7 @@ describe TopicLink do
end end
context "mention links" do context "mention links" do
let(:post) { topic.posts.create(user: user, raw: "Hey #{user.username_lower}") } let(:post) { Fabricate(:post, topic: topic, user: user, raw: "Hey #{user.username_lower}") }
before do before do
TopicLink.extract_from(post) TopicLink.extract_from(post)
@ -275,14 +276,14 @@ describe TopicLink do
context "email address" do context "email address" do
it "does not extract a link" do it "does not extract a link" do
post = topic.posts.create(user: user, raw: "Valid email: foo@bar.com\n\nInvalid email: rfc822;name@domain.com") post = Fabricate(:post, topic: topic, user: user, raw: "Valid email: foo@bar.com\n\nInvalid email: rfc822;name@domain.com")
TopicLink.extract_from(post) TopicLink.extract_from(post)
expect(topic.topic_links).to be_blank expect(topic.topic_links).to be_blank
end end
end end
context "mail link" do context "mail link" do
let(:post) { topic.posts.create(user: user, raw: "[email]bar@example.com[/email]") } let(:post) { Fabricate(:post, topic: topic, user: user, raw: "[email]bar@example.com[/email]") }
it 'does not extract a link' do it 'does not extract a link' do
TopicLink.extract_from(post) TopicLink.extract_from(post)
@ -292,7 +293,7 @@ describe TopicLink do
context "quote links" do context "quote links" do
it "sets quote correctly" do it "sets quote correctly" do
linked_post = topic.posts.create(user: user, raw: "my test post") linked_post = Fabricate(:post, topic: topic, user: user, raw: "my test post")
quoting_post = Fabricate(:post, raw: "[quote=\"#{user.username}, post: #{linked_post.post_number}, topic: #{topic.id}\"]\nquote\n[/quote]") quoting_post = Fabricate(:post, raw: "[quote=\"#{user.username}, post: #{linked_post.post_number}, topic: #{topic.id}\"]\nquote\n[/quote]")
TopicLink.extract_from(quoting_post) TopicLink.extract_from(quoting_post)
@ -304,7 +305,7 @@ describe TopicLink do
end end
context "link to a local attachments" do context "link to a local attachments" do
let(:post) { topic.posts.create(user: user, raw: '<a class="attachment" href="/uploads/default/208/87bb3d8428eb4783.rb?foo=bar">ruby.rb</a>') } let(:post) { Fabricate(:post, topic: topic, user: user, raw: '<a class="attachment" href="/uploads/default/208/87bb3d8428eb4783.rb?foo=bar">ruby.rb</a>') }
it "extracts the link" do it "extracts the link" do
TopicLink.extract_from(post) TopicLink.extract_from(post)
@ -324,7 +325,7 @@ describe TopicLink do
end end
context "link to an attachments uploaded on S3" do context "link to an attachments uploaded on S3" do
let(:post) { topic.posts.create(user: user, raw: '<a class="attachment" href="//s3.amazonaws.com/bucket/2104a0211c9ce41ed67989a1ed62e9a394c1fbd1446.rb">ruby.rb</a>') } let(:post) { Fabricate(:post, topic: topic, user: user, raw: '<a class="attachment" href="//s3.amazonaws.com/bucket/2104a0211c9ce41ed67989a1ed62e9a394c1fbd1446.rb">ruby.rb</a>') }
it "extracts the link" do it "extracts the link" do
TopicLink.extract_from(post) TopicLink.extract_from(post)
@ -347,12 +348,12 @@ describe TopicLink do
describe 'internal link from pm' do describe 'internal link from pm' do
it 'works' do it 'works' do
pm = Fabricate(:topic, user: user, category_id: nil, archetype: 'private_message') pm = Fabricate(:topic, user: user, category_id: nil, archetype: 'private_message')
pm.posts.create(user: user, raw: "some content") Fabricate(:post, topic: pm, user: user, raw: "some content")
url = "http://#{test_uri.host}/t/topic-slug/#{topic.id}" url = "http://#{test_uri.host}/t/topic-slug/#{topic.id}"
pm.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: pm, user: user, raw: 'initial post')
linked_post = pm.posts.create(user: user, raw: "Link to another topic: #{url}") linked_post = Fabricate(:post, topic: pm, user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(linked_post) TopicLink.extract_from(linked_post)
@ -367,8 +368,8 @@ describe TopicLink do
unlisted_topic = Fabricate(:topic, user: user, visible: false) unlisted_topic = Fabricate(:topic, user: user, visible: false)
url = "http://#{test_uri.host}/t/topic-slug/#{topic.id}" url = "http://#{test_uri.host}/t/topic-slug/#{topic.id}"
unlisted_topic.posts.create(user: user, raw: 'initial post') Fabricate(:post, topic: unlisted_topic, user: user, raw: 'initial post')
linked_post = unlisted_topic.posts.create(user: user, raw: "Link to another topic: #{url}") linked_post = Fabricate(:post, topic: unlisted_topic, user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(linked_post) TopicLink.extract_from(linked_post)
@ -384,7 +385,7 @@ describe TopicLink do
alternate_uri = URI.parse(Discourse.base_url) alternate_uri = URI.parse(Discourse.base_url)
url = "http://#{alternate_uri.host}:5678/t/topic-slug/#{other_topic.id}" url = "http://#{alternate_uri.host}:5678/t/topic-slug/#{other_topic.id}"
post = topic.posts.create(user: user, raw: "Link to another topic: #{url}") post = Fabricate(:post, topic: topic, user: user, raw: "Link to another topic: #{url}")
TopicLink.extract_from(post) TopicLink.extract_from(post)
reflection = other_topic.topic_links.first reflection = other_topic.topic_links.first

View File

@ -2933,12 +2933,12 @@ describe Topic do
end end
it 'returns error message if topic has more posts' do it 'returns error message if topic has more posts' do
post_2 = PostCreator.create!(Fabricate(:user), topic_id: topic.id, raw: 'some post content') post_2 = create_post(user: user, topic_id: topic.id, raw: 'some post content')
PostDestroyer.new(admin, post).destroy PostDestroyer.new(admin, post).destroy
expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(I18n.t('post.cannot_permanently_delete.many_posts')) expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(I18n.t('post.cannot_permanently_delete.many_posts'))
PostDestroyer.new(admin, post_2).destroy PostDestroyer.new(admin, post_2.reload).destroy
expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(nil) expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(nil)
end end

View File

@ -287,28 +287,31 @@ describe User do
end end
describe 'delete posts in batches' do describe 'delete posts in batches' do
before_all do fab!(:post1) { Fabricate(:post) }
@post1 = Fabricate(:post) fab!(:user) { post1.user }
@user = @post1.user fab!(:post2) { Fabricate(:post, topic: post1.topic, user: user) }
@post2 = Fabricate(:post, topic: @post1.topic, user: @user) fab!(:post3) { Fabricate(:post, user: user) }
@post3 = Fabricate(:post, user: @user) fab!(:posts) { [post1, post2, post3] }
@posts = [@post1, @post2, @post3] fab!(:post_ids) { [post1.id, post2.id, post3.id] }
@guardian = Guardian.new(Fabricate(:admin)) fab!(:guardian) { Guardian.new(Fabricate(:admin)) }
Fabricate(:reviewable_queued_post, created_by: @user) fab!(:reviewable_queued_post) { Fabricate(:reviewable_queued_post, created_by: user) }
end
it 'deletes only one batch of posts' do it 'deletes only one batch of posts' do
deleted_posts = @user.delete_posts_in_batches(@guardian, 1) post2
expect(Post.where(id: @posts.map(&:id)).count).to eq(2) deleted_posts = user.delete_posts_in_batches(guardian, 1)
expect(Post.where(id: post_ids).count).to eq(2)
expect(deleted_posts.length).to eq(1) expect(deleted_posts.length).to eq(1)
expect(deleted_posts[0]).to eq(@post2) expect(deleted_posts[0]).to eq(post2)
end end
it 'correctly deletes posts and topics' do it 'correctly deletes posts and topics' do
@user.delete_posts_in_batches(@guardian, 20) posts
expect(Post.where(id: @posts.map(&:id))).to be_empty user.delete_posts_in_batches(guardian, 20)
expect(Reviewable.where(created_by: @user).count).to eq(0)
@posts.each do |p| expect(Post.where(id: post_ids)).to be_empty
expect(Reviewable.where(created_by: user).count).to eq(0)
posts.each do |p|
if p.is_first_post? if p.is_first_post?
expect(Topic.find_by(id: p.topic_id)).to be_nil expect(Topic.find_by(id: p.topic_id)).to be_nil
end end
@ -319,10 +322,10 @@ describe User do
invalid_guardian = Guardian.new(Fabricate(:user)) invalid_guardian = Guardian.new(Fabricate(:user))
expect do expect do
@user.delete_posts_in_batches(invalid_guardian) user.delete_posts_in_batches(invalid_guardian)
end.to raise_error Discourse::InvalidAccess end.to raise_error Discourse::InvalidAccess
@posts.each do |p| posts.each do |p|
p.reload p.reload
expect(p).to be_present expect(p).to be_present
expect(p.topic).to be_present expect(p.topic).to be_present

View File

@ -874,6 +874,8 @@ describe PostsController do
end end
describe "when logged in" do describe "when logged in" do
fab!(:user) { Fabricate(:user) }
before do before do
sign_in(user) sign_in(user)
end end
@ -919,7 +921,7 @@ describe PostsController do
end end
it "doesn't enqueue posts when user first creates a topic" do it "doesn't enqueue posts when user first creates a topic" do
user.user_stat.update_column(:topic_count, 1) Fabricate(:topic, user: user)
Draft.set(user, "should_clear", 0, "{'a' : 'b'}") Draft.set(user, "should_clear", 0, "{'a' : 'b'}")

View File

@ -112,7 +112,7 @@ describe PostOwnerChanger do
p1user.user_stat.update!( p1user.user_stat.update!(
topic_count: 1, topic_count: 1,
post_count: 1, post_count: 0,
first_post_created_at: p1.created_at, first_post_created_at: p1.created_at,
) )
@ -151,7 +151,7 @@ describe PostOwnerChanger do
expect(p2user.topic_count).to eq(0) expect(p2user.topic_count).to eq(0)
expect(p2user.post_count).to eq(0) expect(p2user.post_count).to eq(0)
expect(user_a.topic_count).to eq(1) expect(user_a.topic_count).to eq(1)
expect(user_a.post_count).to eq(2) expect(user_a.post_count).to eq(1)
p1_user_stat = p1user.user_stat p1_user_stat = p1user.user_stat